Repository: crawler-commons/crawler-commons
Pull request #218 (2018-09-28 10:24:39)

Support sitemap extensions
- adds optionally support for image, video, news, mobile sitemaps and alternate links in sitemaps
- the solution is taken from #162 - thanks, @tuxnco! Based on the attribute classes and the unit tests it was pretty straight-forward to port the code to the current master and from DOM to SAX parser. For additional modifications, see the commit messages and the discussions in #162 - thanks also to all reviewers (@MichealKum, @jnioche, @lewismc).

Support for sitemap extensions needs to be enabled in the parser, see the methods SiteMapParser.enableExtension(extension) and .enableExtensions(). This registers the namespace of the extension(s) and lets the XMLHandler then call the corresponding ExtensionHandler.


Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index f992e96..16bf0a8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,7 @@
 Crawler-Commons Change Log
 
 Current Development 0.11-SNAPSHOT (yyyy-mm-dd)
+- [Sitemaps] Add support for sitemap extensions (tuxnco, sebastian-nagel) #35, #36, #149, #162
 - [Sitemaps] Use the Java 8 date and time API (java.time.*) to parse dates in sitemaps (sebastian-nagel) #217
 - [Robots] Fix for handling URLs with query parameters but no path (kkrugler) #215
 
diff --git a/src/main/java/crawlercommons/sitemaps/AbstractSiteMap.java b/src/main/java/crawlercommons/sitemaps/AbstractSiteMap.java
index cabf933..46a1467 100644
--- a/src/main/java/crawlercommons/sitemaps/AbstractSiteMap.java
+++ b/src/main/java/crawlercommons/sitemaps/AbstractSiteMap.java
@@ -153,7 +153,8 @@ public abstract class AbstractSiteMap {
      * Dates must follow the <a href="https://www.w3.org/TR/NOTE-datetime">W3C
      * Datetime format</a> which is similar to <a
      * href="https://en.wikipedia.org/wiki/ISO_8601">ISO-8601</a> but allows
-     * dates with different precisions:</p>
+     * dates with different precisions:
+     * </p>
      * 
      * <pre>
      *   Year:
diff --git a/src/main/java/crawlercommons/sitemaps/Namespace.java b/src/main/java/crawlercommons/sitemaps/Namespace.java
index e69817e..be554c3 100644
--- a/src/main/java/crawlercommons/sitemaps/Namespace.java
+++ b/src/main/java/crawlercommons/sitemaps/Namespace.java
@@ -18,7 +18,12 @@ package crawlercommons.sitemaps;
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.TreeMap;
+
+import crawlercommons.sitemaps.extension.Extension;
 
 /**
  * supported sitemap formats:
@@ -60,6 +65,11 @@ public class Namespace {
                     "http://www.google.com/schemas/sitemap-news/0.84" //
     };
 
+    public static final String[] MOBILE = { //
+                    "http://www.google.com/schemas/sitemap-mobile/1.0", //
+                    "https://www.google.com/schemas/sitemap-mobile/1.0" //
+    };
+
     public static final String LINKS = "http://www.w3.org/1999/xhtml";
 
     /**
@@ -103,4 +113,12 @@ public class Namespace {
         return SITEMAP_SUPPORTED_NAMESPACES.contains(uri);
     }
 
+    public static final Map<Extension, List<String>> SITEMAP_EXTENSION_NAMESPACES = new TreeMap<>();
+    static {
+        SITEMAP_EXTENSION_NAMESPACES.put(Extension.NEWS, Arrays.asList(NEWS));
+        SITEMAP_EXTENSION_NAMESPACES.put(Extension.IMAGE, Arrays.asList(IMAGE));
+        SITEMAP_EXTENSION_NAMESPACES.put(Extension.VIDEO, Arrays.asList(VIDEO));
+        SITEMAP_EXTENSION_NAMESPACES.put(Extension.MOBILE, Arrays.asList(MOBILE));
+        SITEMAP_EXTENSION_NAMESPACES.put(Extension.LINKS, Arrays.asList(LINKS));
+    }
 }
diff --git a/src/main/java/crawlercommons/sitemaps/SiteMapParser.java b/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
index 1788e5d..77a8138 100644
--- a/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
+++ b/src/main/java/crawlercommons/sitemaps/SiteMapParser.java
@@ -28,8 +28,10 @@ import java.io.StringReader;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.zip.GZIPInputStream;
@@ -48,6 +50,7 @@ import org.xml.sax.SAXException;
 
 import crawlercommons.mimetypes.MimeTypeDetector;
 import crawlercommons.sitemaps.AbstractSiteMap.SitemapType;
+import crawlercommons.sitemaps.extension.Extension;
 import crawlercommons.sitemaps.sax.DelegatorHandler;
 
 public class SiteMapParser {
@@ -84,6 +87,12 @@ public class SiteMapParser {
     /** Set of namespaces (if {@link #strictNamespace}) accepted by the parser. URLs from other namespaces are ignored. */
     protected Set<String> acceptedNamespaces = new HashSet<>();
 
+    /**
+     * Map of sitemap extension namespaces required to find the right extension
+     * handler.
+     */
+    protected Map<String, Extension> extensionNamespaces = new HashMap<>();
+
     private MimeTypeDetector mimeTypeDetector;
 
     public SiteMapParser() {
@@ -158,6 +167,29 @@ public class SiteMapParser {
         }
     }
 
+    /**
+     * Enable a support for a sitemap extension in the parser.
+     * 
+     * @param extension
+     *            sitemap extension (news, images, videos, etc.)
+     */
+    public void enableExtension(Extension extension) {
+        for (String namespaceUri : Namespace.SITEMAP_EXTENSION_NAMESPACES.get(extension)) {
+            extensionNamespaces.put(namespaceUri, extension);
+        }
+    }
+
+    /**
+     * Enable all supported sitemap extensions in the parser.
+     */
+    public void enableExtensions() {
+        for (Extension extension : Extension.values()) {
+            for (String namespaceUri : Namespace.SITEMAP_EXTENSION_NAMESPACES.get(extension)) {
+                extensionNamespaces.put(namespaceUri, extension);
+            }
+        }
+    }
+
     /**
      * Returns a SiteMap or SiteMapIndex given an online sitemap URL
      *
@@ -511,6 +543,7 @@ public class SiteMapParser {
         if (isStrictNamespace()) {
             handler.setAcceptedNamespaces(acceptedNamespaces);
         }
+        handler.setExtensionNamespaces(extensionNamespaces);
 
         try {
             SAXParser saxParser = factory.newSAXParser();
diff --git a/src/main/java/crawlercommons/sitemaps/SiteMapURL.java b/src/main/java/crawlercommons/sitemaps/SiteMapURL.java
index f1a4876..7f8fc1c 100644
--- a/src/main/java/crawlercommons/sitemaps/SiteMapURL.java
+++ b/src/main/java/crawlercommons/sitemaps/SiteMapURL.java
@@ -24,6 +24,12 @@ import java.net.URL;
 import java.time.ZonedDateTime;
 import java.util.Date;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import crawlercommons.sitemaps.extension.Extension;
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
 
 /**
  * The SitemapUrl class represents a URL found in a Sitemap.
@@ -69,6 +75,11 @@ public class SiteMapURL {
      */
     private boolean valid;
 
+    /**
+     * attributes from sitemap extensions (news, image, video sitemaps, etc.)
+     */
+    private Map<Extension, ExtensionMetadata[]> attributes;
+
     public SiteMapURL(String url, boolean valid) {
         setUrl(url);
         setValid(valid);
@@ -295,6 +306,45 @@ public class SiteMapURL {
         return valid;
     }
 
+    /**
+     * Add attributes of a specific sitemap extension
+     * 
+     * @param extension
+     *            sitemap extension (news, images, videos, etc.)
+     * @param attributes
+     *            array of attributes
+     */
+    public void addAttributesForExtension(Extension extension, ExtensionMetadata[] attributes) {
+        if (this.attributes == null) {
+            this.attributes = new TreeMap<>();
+        }
+        this.attributes.put(extension, attributes);
+    }
+
+    /**
+     * Get attributes of sitemap extensions (news, images, videos, etc.)
+     * 
+     * @return attribute map or null if no extensions are used
+     */
+    public Map<Extension, ExtensionMetadata[]> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Get attributes of a specific sitemap extension
+     * 
+     * @param extension
+     *            sitemap extension (news, images, videos, etc.)
+     * @return array of attributes or null if there are no attributes for the
+     *         given extension
+     */
+    public ExtensionMetadata[] getAttributesForExtension(Extension extension) {
+        if (attributes == null) {
+            return null;
+        }
+        return attributes.get(extension);
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o)
@@ -322,6 +372,13 @@ public class SiteMapURL {
         sb.append(", lastMod = ").append((lastModified == null) ? "null" : SiteMap.W3C_FULLDATE_FORMATTER_UTC.format(lastModified.toInstant()));
         sb.append(", changeFreq = ").append(changeFreq);
         sb.append(", priority = ").append(priority);
+        if (attributes != null) {
+            for (Entry<Extension, ExtensionMetadata[]> e : attributes.entrySet()) {
+                for (ExtensionMetadata m : e.getValue()) {
+                    sb.append(", ").append(m.toString());
+                }
+            }
+        }
 
         return sb.toString();
     }
diff --git a/src/main/java/crawlercommons/sitemaps/extension/Extension.java b/src/main/java/crawlercommons/sitemaps/extension/Extension.java
new file mode 100644
index 0000000..2e8a49a
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/Extension.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+/** Sitemap extensions supported by the parser. */
+public enum Extension {
+    /**
+     * Google News sitemaps, see
+     * https://support.google.com/news/publisher-center/answer/74288
+     */
+    NEWS,
+    /**
+     * Google Image sitemaps, see
+     * https://support.google.com/webmasters/answer/178636
+     */
+    IMAGE,
+    /**
+     * Google Video sitemaps, see
+     * https://support.google.com/webmasters/answer/80471
+     */
+    VIDEO,
+    /**
+     * Usage of <code>&lt;xhtml:links&gt;</code> in sitemaps to include
+     * localized page versions/variants, see
+     * https://support.google.com/webmasters/answer/189077
+     */
+    LINKS,
+    /**
+     * <cite>Mobile sitemaps just contain an empty "mobile" tag to identify a
+     * URL as having mobile content</cite>, cf.
+     * http://www.google.com/schemas/sitemap-mobile/1.0
+     */
+    MOBILE
+}
diff --git a/src/main/java/crawlercommons/sitemaps/extension/ExtensionMetadata.java b/src/main/java/crawlercommons/sitemaps/extension/ExtensionMetadata.java
new file mode 100644
index 0000000..aaa83b7
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/ExtensionMetadata.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+import crawlercommons.sitemaps.SiteMapURL;
+
+/**
+ * Container for attributes of a {@link SiteMapURL} defined by a sitemap
+ * extension.
+ */
+public abstract class ExtensionMetadata {
+
+    public abstract boolean equals(Object other);
+
+    public boolean isValid() {
+        return true;
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/extension/ImageAttributes.java b/src/main/java/crawlercommons/sitemaps/extension/ImageAttributes.java
new file mode 100644
index 0000000..7f20bb2
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/ImageAttributes.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+import java.net.URL;
+import java.util.Objects;
+
+/**
+ * Data model for Google extension to the sitemap protocol regarding images
+ * indexing, as per http://www.google.com/schemas/sitemap-image/1.1
+ */
+public class ImageAttributes extends ExtensionMetadata {
+    /**
+     * Image location attribute found under image/loc (required)
+     */
+    private URL loc;
+
+    /**
+     * Image caption attribute found under image/caption (optional)
+     */
+    private String caption;
+
+    /**
+     * Image geo location attribute found under image/geo_location (optional)
+     */
+    private String geoLocation;
+
+    /**
+     * Image title attribute found under image/title (optional)
+     */
+    private String title;
+
+    /**
+     * Image license attribute found under image/license (optional)
+     */
+    private URL license;
+
+    public URL getLoc() {
+        return loc;
+    }
+
+    public void setLoc(URL loc) {
+        this.loc = loc;
+    }
+
+    public String getCaption() {
+        return caption;
+    }
+
+    public void setCaption(String caption) {
+        this.caption = caption;
+    }
+
+    public String getGeoLocation() {
+        return geoLocation;
+    }
+
+    public void setGeoLocation(String geoLocation) {
+        this.geoLocation = geoLocation;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public URL getLicense() {
+        return license;
+    }
+
+    public void setLicense(URL license) {
+        this.license = license;
+    }
+
+    public ImageAttributes() {
+    }
+
+    public ImageAttributes(URL loc) {
+        this.loc = loc;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Image loc: ").append(loc);
+        sb.append(", caption: ").append(caption);
+        sb.append(", title: ").append(title);
+        sb.append(", geoLocation: ").append(geoLocation);
+        sb.append(", license: ").append(license);
+        return sb.toString();
+    }
+
+    @Override
+    public boolean isValid() {
+        return loc != null;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof ImageAttributes)) {
+            return false;
+        }
+        ImageAttributes that = (ImageAttributes) other;
+        return Objects.equals(loc, that.loc) //
+                        && Objects.equals(caption, that.caption) //
+                        && Objects.equals(geoLocation, that.geoLocation) //
+                        && Objects.equals(title, that.title) //
+                        && Objects.equals(license, that.license);
+    }
+
+}
\ No newline at end of file
diff --git a/src/main/java/crawlercommons/sitemaps/extension/LinkAttributes.java b/src/main/java/crawlercommons/sitemaps/extension/LinkAttributes.java
new file mode 100644
index 0000000..245a110
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/LinkAttributes.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+import java.net.URL;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+/**
+ * Data model for Google extension to the sitemap protocol regarding alternate
+ * links indexing. Cf. https://support.google.com/webmasters/answer/189077:
+ * <blockquote>Each <code>&lt;url&gt;</code> element must have a child element:
+ * <code>&lt;xhtml:link rel="alternate" hreflang="supported_language-code"&gt;</code>
+ * that lists every alternate version of the page, including itself. The order
+ * of these child <code>&lt;xhtml:link&gt;</code> elements doesn't matter,
+ * though you might want to keep them in the same order to make them easier for
+ * you to check for mistakes.</blockquote>
+ */
+public class LinkAttributes extends ExtensionMetadata {
+    /**
+     * Link's href attribute
+     */
+    private URL href;
+
+    /**
+     * Link's other attributes key and values
+     */
+    private Map<String, String> params;
+
+    public LinkAttributes() {
+    }
+
+    public LinkAttributes(URL href) {
+        this.href = href;
+    }
+
+    public URL getHref() {
+        return href;
+    }
+
+    public void setHref(URL href) {
+        this.href = href;
+    }
+
+    public Map<String, String> getParams() {
+        return params;
+    }
+
+    public void setParams(Map<String, String> params) {
+        this.params = params;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("Links href: ").append(href);
+        if (params != null && !params.isEmpty()) {
+            sb.append(", params: ");
+            boolean first = true;
+            for (Entry<String, String> e : params.entrySet()) {
+                if (!first) {
+                    sb.append(',');
+                }
+                sb.append(e.getKey()).append(':').append(e.getValue());
+                first = false;
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof LinkAttributes)) {
+            return false;
+        }
+        LinkAttributes that = (LinkAttributes) other;
+        return Objects.equals(href, that.href) //
+                        && Objects.equals(params, that.params);
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/extension/MobileAttributes.java b/src/main/java/crawlercommons/sitemaps/extension/MobileAttributes.java
new file mode 100644
index 0000000..32e7eb3
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/MobileAttributes.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+/**
+ * Google mobile sitemap attributes, see
+ * http://www.google.de/schemas/sitemap-mobile/1.0/ and
+ * https://www.google.com/schemas/sitemap-mobile/1.0/sitemap-mobile.xsd:
+ * <blockquote>Mobile sitemaps just contain an empty "mobile" tag to identify a
+ * URL as having mobile content.</blockquote>
+ */
+public class MobileAttributes extends ExtensionMetadata {
+
+    @Override
+    public String toString() {
+        return "Mobile content avaiblabe: yes";
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof MobileAttributes)) {
+            return false;
+        }
+        return true;
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/extension/NewsAttributes.java b/src/main/java/crawlercommons/sitemaps/extension/NewsAttributes.java
new file mode 100644
index 0000000..0596596
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/NewsAttributes.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package crawlercommons.sitemaps.extension;
+
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Data model for Google's extension to the sitemap protocol regarding news
+ * indexing, as per http://www.google.com/schemas/sitemap-news/0.9
+ */
+public class NewsAttributes extends ExtensionMetadata {
+
+    public static enum NewsGenre {
+        Blog, OpEd, Opinion, PressRelease, Satire, UserGenerated
+    }
+
+    /**
+     * News publication name found under news/publication/name (required)
+     */
+    private String name;
+
+    /**
+     * News publication language found under news/publication/language
+     * (required)
+     */
+    private String language;
+
+    /**
+     * News genres found under news/genres (required if applicable)
+     */
+    private NewsGenre[] genres;
+
+    /**
+     * News publication date found under news/publication_date (required)
+     */
+    private ZonedDateTime publicationDate;
+
+    /**
+     * News title found under news/title (required)
+     */
+    private String title;
+
+    /**
+     * News keywords found under news/keywords (optional)
+     * 
+     * @see https://support.google.com/news/publisher/answer/116037 for examples
+     */
+    private String[] keywords;
+
+    /**
+     * News stock tickers found under news/stock_tickers (optional)
+     */
+    private String[] stockTickers;
+
+    public NewsAttributes() {
+    }
+
+    public NewsAttributes(final String name, final String language, final ZonedDateTime publicationDate, final String title) {
+        this.name = name;
+        this.language = language;
+        this.publicationDate = publicationDate;
+        this.title = title;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public NewsGenre[] getGenres() {
+        return genres;
+    }
+
+    public void setGenres(NewsGenre[] genres) {
+        this.genres = genres;
+    }
+
+    public Date getPublicationDate() {
+        return Date.from(publicationDate.toInstant());
+    }
+
+    public ZonedDateTime getPublicationDateTime() {
+        return publicationDate;
+    }
+
+    public void setPublicationDate(ZonedDateTime publicationDate) {
+        this.publicationDate = publicationDate;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String[] getKeywords() {
+        return keywords;
+    }
+
+    public void setKeywords(String[] keywords) {
+        this.keywords = keywords;
+    }
+
+    public String[] getStockTickers() {
+        return stockTickers;
+    }
+
+    public void setStockTickers(String[] stockTickers) {
+        this.stockTickers = stockTickers;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null || !(other instanceof NewsAttributes)) {
+            return false;
+        }
+        NewsAttributes that = (NewsAttributes) other;
+        return Objects.equals(name, that.name) //
+                        && Objects.equals(language, that.language) //
+                        && Objects.equals(title, that.title) //
+                        && Objects.equals(publicationDate, publicationDate) //
+                        && Objects.deepEquals(keywords, that.keywords) //
+                        && Objects.deepEquals(genres, that.genres) //
+                        && Objects.deepEquals(stockTickers, that.stockTickers);
+    }
+
+    @Override
+    public boolean isValid() {
+        return name != null && language != null && publicationDate != null && title != null;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("News name: ").append(name);
+        sb.append(", title: ").append(title);
+        sb.append(", language: ").append(language);
+        sb.append(", publication-date: ").append(publicationDate);
+        if (keywords != null) {
+            sb.append(", keywords: ").append(String.join(", ", keywords));
+        }
+        // Arrays.asList(genres).stream().map(NewsGenre::toString);
+        if (genres != null) {
+            sb.append(", genres: ");
+            for (int i = 0; i < genres.length; i++) {
+                if (i > 0) {
+                    sb.append(',');
+                }
+                sb.append(genres[i].toString());
+            }
+        }
+        if (stockTickers != null) {
+            sb.append(", keywords: ").append(String.join(", ", stockTickers));
+        }
+        return sb.toString();
+    }
+}
diff --git a/src/main/java/crawlercommons/sitemaps/extension/VideoAttributes.java b/src/main/java/crawlercommons/sitemaps/extension/VideoAttributes.java
new file mode 100644
index 0000000..2f6e810
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/extension/VideoAttributes.java
@@ -0,0 +1,548 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.extension;
+
+import java.net.URL;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Data model for Google extension to the sitemap protocol regarding images
+ * indexing, as per http://www.google.com/schemas/sitemap-video/1.1
+ */
+public class VideoAttributes extends ExtensionMetadata {
+
+    /**
+     * Video thumbnail URL found under video/thumbnail_loc (required)
+     */
+    private URL thumbnailLoc;
+
+    /**
+     * Video title found under video/title (required)
+     */
+    private String title;
+
+    /**
+     * Video description found under video/description (required)
+     */
+    private String description;
+
+    /**
+     * Video content location found under video/content_loc (depends) if not
+     * specified, player location must be specified
+     */
+    private URL contentLoc;
+
+    /**
+     * Video player location found under video/player_loc (depends) if not
+     * specified, content location must be specified
+     */
+    private URL playerLoc;
+
+    /**
+     * Video duration in seconds found under video/duration (recommended) Must
+     * be integer between 0 and 28800 (8 hours)
+     */
+    private Integer duration;
+
+    /**
+     * Video expiration date found under video/expiration_date (recommended if
+     * applicable)
+     */
+    private ZonedDateTime expirationDate;
+
+    /**
+     * Video rating found under video/rating (optional) Must be float value
+     * between 0.0 and 5.0
+     */
+    private Float rating;
+
+    /**
+     * Video view count found under video/view_count (optional)
+     */
+    private Integer viewCount;
+
+    /**
+     * Video publication date found under video/publication_date (optional)
+     */
+    private ZonedDateTime publicationDate;
+
+    /**
+     * Video family friendly attribute found under video/family_friendly
+     * (optional)
+     */
+    private Boolean familyFriendly;
+
+    /**
+     * Video tags found under video/tag (optional) Up to 32 tags can be
+     * specified
+     */
+    private String[] tags;
+
+    /**
+     * Video category found under video/category (optional)
+     */
+    private String category;
+
+    /**
+     * Video restricted countries found under video/restriction (optional)
+     * blacklist of countries filled if video/restriction node has an attribute
+     * named relationship with a value of deny.
+     */
+    private String[] restrictedCountries;
+
+    /**
+     * Video allowed countries found under video/restriction (optional)
+     * whitelist of countries filled if video/restriction node has an attribute
+     * named relationship with a value of allow.
+     */
+    private String[] allowedCountries;
+
+    /**
+     * Video gallery location found under video/gallery_loc (optional)
+     */
+    private URL galleryLoc;
+
+    /**
+     * Video gallery title found under video/gallery_loc[@title] (optional)
+     */
+    private String galleryTitle;
+
+    /**
+     * Video prices found under video/price (optional)
+     */
+    private VideoPrice[] prices;
+
+    /**
+     * Video requires subscription (free or paid) found under
+     * video/requires_subscription (optional)
+     */
+    private Boolean requiresSubscription;
+
+    /**
+     * Video uploader found under video/uploader (optional)
+     */
+    private String uploader;
+
+    /**
+     * Video uploader location (optional) Must be on the same domain as the
+     * &lt;loc&gt; this property refers to
+     */
+    private URL uploaderInfo;
+
+    /**
+     * Video restricted platforms found under video/platform (optional)
+     * blacklist of platform filled if video/platform node has an attribute
+     * named relationship with a value of deny.
+     */
+    private String[] restrictedPlatforms;
+
+    /**
+     * Video allowed platforms found under video/platform (optional) whitelist
+     * of platforms filled if video/platform node has an attribute named
+     * relationship with a value of allow.
+     */
+    private String[] allowedPlatforms;
+
+    /**
+     * Video is a live stream found under video/live (optional)
+     */
+    private Boolean isLive;
+
+    public URL getThumbnailLoc() {
+        return thumbnailLoc;
+    }
+
+    public void setThumbnailLoc(URL thumbnailLoc) {
+        this.thumbnailLoc = thumbnailLoc;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public URL getContentLoc() {
+        return contentLoc;
+    }
+
+    public void setContentLoc(URL contentLoc) {
+        this.contentLoc = contentLoc;
+    }
+
+    public URL getPlayerLoc() {
+        return playerLoc;
+    }
+
+    public void setPlayerLoc(URL playerLoc) {
+        this.playerLoc = playerLoc;
+    }
+
+    public Integer getDuration() {
+        return duration;
+    }
+
+    public void setDuration(Integer duration) {
+        this.duration = duration;
+    }
+
+    public Date getExpirationDate() {
+        return Date.from(expirationDate.toInstant());
+    }
+
+    public ZonedDateTime getExpirationDateTime() {
+        return expirationDate;
+    }
+
+    public void setExpirationDate(ZonedDateTime expirationDate) {
+        this.expirationDate = expirationDate;
+    }
+
+    public Float getRating() {
+        return rating;
+    }
+
+    public void setRating(Float rating) {
+        this.rating = rating;
+    }
+
+    public Integer getViewCount() {
+        return viewCount;
+    }
+
+    public void setViewCount(Integer viewCount) {
+        this.viewCount = viewCount;
+    }
+
+    public Date getPublicationDate() {
+        return Date.from(publicationDate.toInstant());
+    }
+
+    public ZonedDateTime getPublicationDateTime() {
+        return publicationDate;
+    }
+
+    public void setPublicationDate(ZonedDateTime publicationDate) {
+        this.publicationDate = publicationDate;
+    }
+
+    public Boolean getFamilyFriendly() {
+        return familyFriendly;
+    }
+
+    public void setFamilyFriendly(Boolean familyFriendly) {
+        this.familyFriendly = familyFriendly;
+    }
+
+    public String[] getTags() {
+        return tags;
+    }
+
+    public void setTags(String[] tags) {
+        this.tags = tags;
+    }
+
+    public void addTag(String tag) {
+        if (tag == null) {
+            return;
+        }
+        String[] arr;
+        if (tags == null) {
+            arr = new String[1];
+            arr[0] = tag;
+        } else {
+            arr = Arrays.copyOf(tags, tags.length + 1);
+            arr[tags.length] = tag;
+        }
+        tags = arr;
+    }
+
+    public String getCategory() {
+        return category;
+    }
+
+    public void setCategory(String category) {
+        this.category = category;
+    }
+
+    public String[] getRestrictedCountries() {
+        return restrictedCountries;
+    }
+
+    public void setRestrictedCountries(String[] restrictedCountries) {
+        this.restrictedCountries = restrictedCountries;
+    }
+
+    public String[] getAllowedCountries() {
+        return allowedCountries;
+    }
+
+    public void setAllowedCountries(String[] allowedCountries) {
+        this.allowedCountries = allowedCountries;
+    }
+
+    public URL getGalleryLoc() {
+        return galleryLoc;
+    }
+
+    public void setGalleryLoc(URL galleryLoc) {
+        this.galleryLoc = galleryLoc;
+    }
+
+    public String getGalleryTitle() {
+        return galleryTitle;
+    }
+
+    public void setGalleryTitle(String galleryTitle) {
+        this.galleryTitle = galleryTitle;
+    }
+
+    public VideoPrice[] getPrices() {
+        return prices;
+    }
+
+    public void setPrices(VideoPrice[] prices) {
+        this.prices = prices;
+    }
+
+    public void addPrice(VideoPrice price) {
+        if (price == null) {
+            return;
+        }
+        VideoPrice[] arr;
+        if (prices == null) {
+            arr = new VideoPrice[1];
+            arr[0] = price;
+        } else {
+            arr = Arrays.copyOf(prices, prices.length + 1);
+            arr[prices.length] = price;
+        }
+        prices = arr;
+    }
+
+    public Boolean getRequiresSubscription() {
+        return requiresSubscription;
+    }
+
+    public void setRequiresSubscription(Boolean requiresSubscription) {
+        this.requiresSubscription = requiresSubscription;
+    }
+
+    public String getUploader() {
+        return uploader;
+    }
+
+    public void setUploader(String uploader) {
+        this.uploader = uploader;
+    }
+
+    public URL getUploaderInfo() {
+        return uploaderInfo;
+    }
+
+    public void setUploaderInfo(URL uploaderInfo) {
+        this.uploaderInfo = uploaderInfo;
+    }
+
+    public String[] getRestrictedPlatforms() {
+        return restrictedPlatforms;
+    }
+
+    public void setRestrictedPlatforms(String[] restrictedPlatforms) {
+        this.restrictedPlatforms = restrictedPlatforms;
+    }
+
+    public String[] getAllowedPlatforms() {
+        return allowedPlatforms;
+    }
+
+    public void setAllowedPlatforms(String[] allowedPlatforms) {
+        this.allowedPlatforms = allowedPlatforms;
+    }
+
+    public Boolean getLive() {
+        return isLive;
+    }
+
+    public void setLive(Boolean live) {
+        isLive = live;
+    }
+
+    public enum VideoPriceType {
+        own, rent
+    }
+
+    public enum VideoPriceResolution {
+        SD, HD
+    }
+
+    public static final class VideoPrice {
+        /**
+         * Video price currency found under video/price[@currency] (required)
+         */
+        private final String currency;
+
+        /**
+         * Video price type (rent vs own) found under video/price[@type]
+         * (optional, defaults to own)
+         */
+        private final VideoPriceType type;
+
+        /**
+         * Video price resolution found under video/price[@resolution]
+         */
+        private final VideoPriceResolution resolution;
+
+        /**
+         * Video price
+         */
+        private float price;
+
+        public VideoPrice(final String currency, final float price) {
+            this(currency, price, VideoPriceType.own);
+        }
+
+        public VideoPrice(final String currency, final float price, final VideoPriceType type) {
+            this(currency, price, type, null);
+        }
+
+        public VideoPrice(final String currency, final float price, final VideoPriceType type, final VideoPriceResolution resolution) {
+            this.currency = currency;
+            this.price = price;
+            this.type = type;
+            this.resolution = resolution;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.ENGLISH, "value: %.2f, currency: %s, type: %s, resolution: %s", price, currency, type, resolution);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null) {
+                return false;
+            }
+            if (!(other instanceof VideoPrice)) {
+                return false;
+            }
+            VideoPrice that = (VideoPrice) other;
+            return Objects.equals(currency, that.currency) //
+                            && price == that.price //
+                            && type == that.type //
+                            && Objects.equals(resolution, that.resolution);
+        }
+
+        public String getCurrency() {
+            return currency;
+        }
+
+        public VideoPriceType getType() {
+            return type;
+        }
+
+        public VideoPriceResolution getResolution() {
+            return resolution;
+        }
+
+        public float getPrice() {
+            return price;
+        }
+
+        public void setPrice(float price) {
+            this.price = price;
+        }
+
+    }
+
+    public VideoAttributes() {
+    }
+
+    public VideoAttributes(final URL thumbnailLoc, final String title, final String description, final URL contentLoc, final URL playerLoc) {
+        this.thumbnailLoc = thumbnailLoc;
+        this.title = title;
+        this.description = description;
+        this.contentLoc = contentLoc;
+        this.playerLoc = playerLoc;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("Video title: ").append(title) //
+                        .append(", description: ").append(description) //
+                        .append(", thumbnail: ").append(thumbnailLoc) //
+                        .append(", contentLoc: ").append(contentLoc) //
+                        .append(", playerLoc: ").append(playerLoc) //
+                        .append(", prices: ").append(Arrays.toString(prices)) //
+                        .toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == null) {
+            return false;
+        }
+        if (!(other instanceof VideoAttributes)) {
+            return false;
+        }
+        VideoAttributes that = (VideoAttributes) other;
+        return Objects.equals(thumbnailLoc, that.thumbnailLoc) //
+                        && Objects.equals(title, that.title) //
+                        && Objects.equals(description, that.description) //
+                        && Objects.equals(contentLoc, that.contentLoc) //
+                        && Objects.equals(playerLoc, that.playerLoc) //
+                        && Objects.equals(duration, that.duration) //
+                        && Objects.equals(expirationDate, that.expirationDate) //
+                        && Objects.equals(rating, that.rating) //
+                        && Objects.equals(viewCount, that.viewCount) //
+                        && Objects.equals(publicationDate, that.publicationDate) //
+                        && Objects.equals(familyFriendly, that.familyFriendly) //
+                        && Objects.deepEquals(tags, that.tags) //
+                        && Objects.equals(category, that.category) //
+                        && Objects.deepEquals(restrictedCountries, that.restrictedCountries) //
+                        && Objects.deepEquals(allowedCountries, that.allowedCountries) //
+                        && Objects.equals(galleryLoc, that.galleryLoc) //
+                        && Objects.equals(galleryTitle, that.galleryTitle) //
+                        && Objects.deepEquals(prices, that.prices) //
+                        && Objects.equals(requiresSubscription, that.requiresSubscription) //
+                        && Objects.equals(uploader, that.uploader) //
+                        && Objects.equals(uploaderInfo, that.uploaderInfo) //
+                        && Objects.deepEquals(allowedPlatforms, that.allowedPlatforms) //
+                        && Objects.deepEquals(restrictedPlatforms, that.restrictedPlatforms) //
+                        && Objects.equals(isLive, that.isLive);
+    }
+
+    @Override
+    public boolean isValid() {
+        return thumbnailLoc != null && title != null && title.length() <= 100 && description != null && description.length() <= 2048 && (contentLoc != null || playerLoc != null);
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java b/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
index 3073cd9..32767ef 100644
--- a/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
+++ b/src/main/java/crawlercommons/sitemaps/sax/DelegatorHandler.java
@@ -21,6 +21,7 @@ import static crawlercommons.sitemaps.SiteMapParser.LOG;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.LinkedList;
+import java.util.Map;
 import java.util.Set;
 
 import org.xml.sax.Attributes;
@@ -31,6 +32,7 @@ import org.xml.sax.helpers.DefaultHandler;
 import crawlercommons.sitemaps.AbstractSiteMap;
 import crawlercommons.sitemaps.Namespace;
 import crawlercommons.sitemaps.UnknownFormatException;
+import crawlercommons.sitemaps.extension.Extension;
 
 /**
  * Provides a base SAX handler for parsing of XML documents representing
@@ -45,6 +47,7 @@ public class DelegatorHandler extends DefaultHandler {
     private boolean strictNamespace;
     private UnknownFormatException exception;
     private Set<String> acceptedNamespaces;
+    protected Map<String, Extension> extensionNamespaces;
 
     protected DelegatorHandler(LinkedList<String> elementStack, boolean strict) {
         this.elementStack = elementStack;
@@ -81,6 +84,17 @@ public class DelegatorHandler extends DefaultHandler {
         return acceptedNamespaces.contains(uri);
     }
 
+    public void setExtensionNamespaces(Map<String, Extension> extensionMap) {
+        extensionNamespaces = extensionMap;
+    }
+
+    protected boolean isExtensionNamespace(String uri) {
+        if (extensionNamespaces == null) {
+            return false;
+        }
+        return extensionNamespaces.containsKey(uri);
+    }
+
     protected void setException(UnknownFormatException exception) {
         this.exception = exception;
     }
@@ -153,6 +167,7 @@ public class DelegatorHandler extends DefaultHandler {
                 return;
             }
         }
+        delegate.setExtensionNamespaces(extensionNamespaces);
     }
 
     public void endElement(String uri, String localName, String qName) throws SAXException {
diff --git a/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java b/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
index 7553d62..d1fcfa0 100644
--- a/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
+++ b/src/main/java/crawlercommons/sitemaps/sax/XMLHandler.java
@@ -21,7 +21,12 @@ import static crawlercommons.sitemaps.SiteMapParser.urlIsValid;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
 
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
@@ -31,6 +36,8 @@ import crawlercommons.sitemaps.AbstractSiteMap;
 import crawlercommons.sitemaps.AbstractSiteMap.SitemapType;
 import crawlercommons.sitemaps.SiteMap;
 import crawlercommons.sitemaps.SiteMapURL;
+import crawlercommons.sitemaps.extension.Extension;
+import crawlercommons.sitemaps.sax.extension.ExtensionHandler;
 
 /**
  * Parse XML that contains a valid Sitemap. Example of a Sitemap:
@@ -62,6 +69,8 @@ class XMLHandler extends DelegatorHandler {
     private String priority;
     private int i = 0;
     private boolean currentElementNamespaceIsValid;
+    private String currentElementNamespace;
+    protected Map<Extension, ExtensionHandler> extensionHandlers;
 
     XMLHandler(URL url, LinkedList<String> elementStack, boolean strict) {
         super(elementStack, strict);
@@ -71,7 +80,12 @@ class XMLHandler extends DelegatorHandler {
     }
 
     public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
-        if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
+        currentElementNamespace = uri;
+        if (isExtensionNamespace(uri)) {
+            ExtensionHandler eh = getExtensionHandler(uri);
+            eh.startElement(uri, localName, qName, attributes);
+            return;
+        } else if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
             LOG.debug("Skip element <{}>, namespace <{}> not accepted", localName, uri);
             currentElementNamespaceIsValid = false;
             return;
@@ -98,7 +112,11 @@ class XMLHandler extends DelegatorHandler {
     }
 
     public void endElement(String uri, String localName, String qName) throws SAXException {
-        if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
+        if (isExtensionNamespace(uri)) {
+            ExtensionHandler eh = getExtensionHandler(uri);
+            eh.endElement(uri, localName, qName);
+            return;
+        } else if (isStrictNamespace() && !isAcceptedNamespace(uri)) {
             return;
         }
         if ("url".equals(localName) && "urlset".equals(currentElementParent())) {
@@ -109,7 +127,11 @@ class XMLHandler extends DelegatorHandler {
     }
 
     public void characters(char[] ch, int start, int length) throws SAXException {
-        if (isStrictNamespace() && !currentElementNamespaceIsValid) {
+        if (isExtensionNamespace(currentElementNamespace)) {
+            ExtensionHandler eh = getExtensionHandler(currentElementNamespace);
+            eh.characters(ch, start, length);
+            return;
+        } else if (isStrictNamespace() && !currentElementNamespaceIsValid) {
             return;
         }
         String localName = super.currentElement();
@@ -142,6 +164,11 @@ class XMLHandler extends DelegatorHandler {
                 sUrl.setPriority(priority);
                 sitemap.addSiteMapUrl(sUrl);
                 LOG.debug("  {}. {}", (++i), sUrl);
+                if (extensionHandlers != null) {
+                    for (Entry<Extension, ExtensionHandler> e : extensionHandlers.entrySet()) {
+                        sUrl.addAttributesForExtension(e.getKey(), e.getValue().getAttributes());
+                    }
+                }
             }
         } catch (MalformedURLException e) {
             LOG.debug("Bad url: [{}]", value);
@@ -151,6 +178,48 @@ class XMLHandler extends DelegatorHandler {
             lastMod = null;
             changeFreq = null;
             priority = null;
+            resetExtensionHandlers();
+        }
+    }
+
+    /**
+     * Registers and returns an ExtensionHandler instance bound to this handler
+     * 
+     * @param uri
+     *            URI of sitemap extension namespace
+     * @return handler for the sitemap extension defined by XML namespace
+     */
+    protected ExtensionHandler getExtensionHandler(String uri) {
+        if (extensionNamespaces.containsKey(uri)) {
+            Extension ext = extensionNamespaces.get(uri);
+            if (extensionHandlers == null) {
+                extensionHandlers = new TreeMap<>();
+            }
+            if (!extensionHandlers.containsKey(ext)) {
+                extensionHandlers.put(ext, ExtensionHandler.create(ext));
+            }
+            return extensionHandlers.get(ext);
+        }
+        return null;
+    }
+
+    protected Collection<ExtensionHandler> getExtensionHandlers() {
+        if (extensionHandlers == null) {
+            return new ArrayList<ExtensionHandler>();
+        }
+        return extensionHandlers.values();
+    }
+
+    /**
+     * Reset all extension handlers. Attributes of sitemap extensions are bound
+     * to a single {@link SiteMapURL}, handlers should be reset if a sitemap URL
+     * is closed.
+     */
+    public void resetExtensionHandlers() {
+        if (extensionHandlers != null) {
+            for (Entry<Extension, ExtensionHandler> e : extensionHandlers.entrySet()) {
+                e.getValue().reset();
+            }
         }
     }
 
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/ExtensionHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/ExtensionHandler.java
new file mode 100644
index 0000000..4f4b454
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/ExtensionHandler.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import static crawlercommons.sitemaps.SiteMapParser.LOG;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.xml.sax.helpers.DefaultHandler;
+
+import crawlercommons.sitemaps.SiteMap;
+import crawlercommons.sitemaps.SiteMapURL;
+import crawlercommons.sitemaps.extension.Extension;
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+
+/**
+ * Handler to be called for elements in the namespace of a sitemap extension.
+ * Parses the extension elements and creates extension-specific attributes to be
+ * assigned to a {@link SiteMapURL}.
+ */
+public abstract class ExtensionHandler extends DefaultHandler {
+
+    protected static Pattern commaSeparated = Pattern.compile("\\s*,\\s*");
+
+    protected List<ExtensionMetadata> attributes = new ArrayList<>();
+
+    public static ExtensionHandler create(Extension extension) {
+        switch (extension) {
+            case NEWS:
+                return new NewsHandler();
+            case VIDEO:
+                return new VideoHandler();
+            case IMAGE:
+                return new ImageHandler();
+            case LINKS:
+                return new LinksHandler();
+            case MOBILE:
+                return new MobileHandler();
+            default:
+                return null;
+        }
+    }
+
+    public ExtensionMetadata[] getAttributes() {
+        return attributes.toArray(new ExtensionMetadata[0]);
+    }
+
+    public void reset() {
+        attributes.clear();
+    }
+
+    protected static ZonedDateTime getDateValue(String value) {
+        return SiteMap.convertToZonedDateTime(value);
+    }
+
+    protected static URL getURLValue(final String value) {
+        if (value != null) {
+            try {
+                return new URL(value);
+            } catch (MalformedURLException e) {
+                LOG.debug("Invalid URL value: {}", value);
+            }
+        }
+        return null;
+    }
+
+    protected static Integer getIntegerValue(String value) {
+        if (value != null) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                LOG.debug("Invalid integer value: {}", value);
+            }
+        }
+        return null;
+    }
+
+    protected static Float getFloatValue(String value) {
+        if (value != null) {
+            try {
+                return Float.parseFloat(value);
+            } catch (NumberFormatException e) {
+                LOG.debug("Invalid float value: {}", value);
+            }
+        }
+        return null;
+    }
+
+    protected static Boolean getYesNoBooleanValue(String value, String elemName) {
+        if ("no".equalsIgnoreCase(value)) {
+            return Boolean.FALSE;
+        } else if ("yes".equalsIgnoreCase(value)) {
+            return Boolean.TRUE;
+        } else {
+            LOG.debug("Unexpected value for {} node: {}", elemName, value);
+        }
+        return null;
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/ImageHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/ImageHandler.java
new file mode 100644
index 0000000..aad6032
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/ImageHandler.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+import crawlercommons.sitemaps.extension.ImageAttributes;
+
+/** Handle SAX events in the Google Image sitemap extension namespace. */
+public class ImageHandler extends ExtensionHandler {
+
+    private ImageAttributes currAttr;
+    private StringBuilder currVal;
+
+    public ImageHandler() {
+        reset();
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        resetCurrent();
+    }
+
+    private void resetCurrent() {
+        currAttr = null;
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("image".equals(localName)) {
+            // add last attribute and reset in case of unclosed
+            // elements
+            if (currAttr != null && currAttr.isValid()) {
+                this.attributes.add(currAttr);
+            }
+            resetCurrent();
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        String value = currVal.toString().trim();
+        if ("image".equals(localName)) {
+            if (currAttr != null && currAttr.isValid()) {
+                attributes.add(currAttr);
+            }
+            resetCurrent();
+            return;
+        }
+        if (currAttr == null) {
+            currAttr = new ImageAttributes();
+        }
+        if (value.isEmpty()) {
+            // skip value but reset StringBuilder
+        } else if ("loc".equals(localName)) {
+            currAttr.setLoc(getURLValue(value));
+        } else if ("caption".equals(localName)) {
+            currAttr.setCaption(value);
+        } else if ("title".equals(localName)) {
+            currAttr.setTitle(value);
+        } else if ("geo_location".equals(localName)) {
+            currAttr.setGeoLocation(value);
+        } else if ("license".equals(localName)) {
+            currAttr.setLicense(getURLValue(value));
+        }
+        // reset StringBuilder
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        currVal.append(String.valueOf(ch, start, length));
+    }
+
+    @Override
+    public ExtensionMetadata[] getAttributes() {
+        if (currAttr != null && currAttr.isValid()) {
+            /*
+             * add current element to attribute list, do not reset in case
+             * getAttributes is called during parsing of a sitemap <url> element
+             */
+            attributes.add(currAttr);
+        }
+        return super.getAttributes();
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/LinksHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/LinksHandler.java
new file mode 100644
index 0000000..951f549
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/LinksHandler.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import java.net.URL;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import crawlercommons.sitemaps.extension.LinkAttributes;
+
+/** Handle SAX events in the Google Image sitemap extension namespace. */
+public class LinksHandler extends ExtensionHandler {
+
+    public LinksHandler() {
+        reset();
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("link".equals(localName)) {
+            String href = attributes.getValue("href");
+            if (href != null && !href.trim().isEmpty()) {
+                URL url = getURLValue(href.trim());
+                if (url != null) {
+                    LinkAttributes attr = new LinkAttributes(url);
+                    this.attributes.add(attr);
+                    Map<String, String> params = new TreeMap<>();
+                    for (int i = 0; i < attributes.getLength(); i++) {
+                        String k = attributes.getLocalName(i);
+                        if (!k.equals("href")) {
+                            params.put(k, attributes.getValue(i).trim());
+                        }
+                    }
+                    attr.setParams(params);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/MobileHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/MobileHandler.java
new file mode 100644
index 0000000..3ab8424
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/MobileHandler.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+import crawlercommons.sitemaps.extension.MobileAttributes;
+
+/** Handle SAX events in the Google Mobile sitemap extension namespace. */
+public class MobileHandler extends ExtensionHandler {
+
+    private static MobileAttributes[] noMobileAttributes = new MobileAttributes[0];
+    private static MobileAttributes[] mobileAttributes = new MobileAttributes[1];
+    static {
+        mobileAttributes[0] = new MobileAttributes();
+    }
+
+    private boolean mobileElementFound = false;
+
+    public MobileHandler() {
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("mobile".equals(localName)) {
+            mobileElementFound = true;
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+    }
+
+    public ExtensionMetadata[] getAttributes() {
+        if (mobileElementFound) {
+            return mobileAttributes;
+        }
+        return noMobileAttributes;
+    }
+
+    public void reset() {
+        super.reset();
+        mobileElementFound = false;
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/NewsHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/NewsHandler.java
new file mode 100644
index 0000000..5d3f5e0
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/NewsHandler.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import static crawlercommons.sitemaps.SiteMapParser.LOG;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+import crawlercommons.sitemaps.extension.NewsAttributes;
+
+/** Handle SAX events in the Google News sitemap extension namespace. */
+public class NewsHandler extends ExtensionHandler {
+
+    private NewsAttributes currAttr;
+    private StringBuilder currVal;
+
+    public NewsHandler() {
+        reset();
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        resetCurrent();
+    }
+
+    private void resetCurrent() {
+        currAttr = null;
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("news".equals(localName)) {
+            // add last attribute and reset in case of unclosed
+            // elements
+            if (currAttr != null && currAttr.isValid()) {
+                this.attributes.add(currAttr);
+            }
+            resetCurrent();
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        String value = currVal.toString().trim();
+        if ("news".equals(localName)) {
+            if (currAttr != null && currAttr.isValid()) {
+                attributes.add(currAttr);
+            }
+            resetCurrent();
+            return;
+        }
+        if (currAttr == null) {
+            currAttr = new NewsAttributes();
+        }
+        if (value.isEmpty()) {
+            // skip value but reset StringBuilder
+        } else if ("name".equals(localName)) {
+            currAttr.setName(value);
+        } else if ("title".equals(localName)) {
+            currAttr.setTitle(value);
+        } else if ("language".equals(localName)) {
+            currAttr.setLanguage(value);
+        } else if ("publication_date".equals(localName)) {
+            currAttr.setPublicationDate(getDateValue(value));
+        } else if ("genres".equals(localName)) {
+            String[] genresList = commaSeparated.split(value);
+            List<NewsAttributes.NewsGenre> _genres = new ArrayList<>();
+            for (String genre : genresList) {
+                try {
+                    _genres.add(NewsAttributes.NewsGenre.valueOf(genre.trim()));
+                } catch (IllegalArgumentException e) {
+                    LOG.debug("Unsupported news sitemap genre: {}", genre);
+                }
+            }
+            currAttr.setGenres(_genres.toArray(new NewsAttributes.NewsGenre[_genres.size()]));
+        } else if ("keywords".equals(localName)) {
+            currAttr.setKeywords(commaSeparated.split(value));
+        } else if ("stock_tickers".equals(localName)) {
+            String[] stockTickers = commaSeparated.split(value);
+            if (stockTickers.length > 5) {
+                stockTickers = Arrays.copyOfRange(stockTickers, 0, 5);
+            }
+            currAttr.setStockTickers(stockTickers);
+        }
+        // reset StringBuilder
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        currVal.append(String.valueOf(ch, start, length));
+    }
+
+    @Override
+    public ExtensionMetadata[] getAttributes() {
+        if (currAttr != null && currAttr.isValid()) {
+            /*
+             * add current element to attribute list, do not reset in case
+             * getAttributes is called during parsing of a sitemap <url> element
+             */
+            attributes.add(currAttr);
+        }
+        return super.getAttributes();
+    }
+
+}
diff --git a/src/main/java/crawlercommons/sitemaps/sax/extension/VideoHandler.java b/src/main/java/crawlercommons/sitemaps/sax/extension/VideoHandler.java
new file mode 100644
index 0000000..7405f98
--- /dev/null
+++ b/src/main/java/crawlercommons/sitemaps/sax/extension/VideoHandler.java
@@ -0,0 +1,215 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps.sax.extension;
+
+import static crawlercommons.sitemaps.SiteMapParser.LOG;
+
+import java.time.ZonedDateTime;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+
+import crawlercommons.sitemaps.SiteMap;
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+import crawlercommons.sitemaps.extension.VideoAttributes;
+import crawlercommons.sitemaps.extension.VideoAttributes.VideoPrice;
+import crawlercommons.sitemaps.extension.VideoAttributes.VideoPriceResolution;
+import crawlercommons.sitemaps.extension.VideoAttributes.VideoPriceType;
+
+/** Handle SAX events in the Google Video sitemap extension namespace. */
+public class VideoHandler extends ExtensionHandler {
+
+    private VideoAttributes currAttr;
+    private StringBuilder currVal;
+    private String relationAttr;
+    private Map<String, String> priceAttr;
+    private static String[] PRICE_ATTRIBUTES = { "currency", "type", "resolution" };
+
+    public VideoHandler() {
+        reset();
+    }
+
+    @Override
+    public void reset() {
+        super.reset();
+        resetCurrent();
+    }
+
+    private void resetCurrent() {
+        currAttr = null;
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+        if ("video".equals(localName)) {
+            // add last attribute and reset in case of unclosed
+            // elements
+            if (currAttr != null && currAttr.isValid()) {
+                this.attributes.add(currAttr);
+            }
+            resetCurrent();
+            return;
+        }
+        if (currAttr == null) {
+            currAttr = new VideoAttributes();
+        }
+        if ("restriction".equals(localName) || "platform".equals(localName)) {
+            relationAttr = attributes.getValue("relationship");
+        } else if ("gallery_loc".equals(localName)) {
+            currAttr.setGalleryTitle(attributes.getValue("title"));
+        } else if ("uploader".equals(localName)) {
+            currAttr.setUploaderInfo(getURLValue(attributes.getValue("info")));
+        } else if ("price".equals(localName)) {
+            priceAttr = new TreeMap<>();
+            for (String a : PRICE_ATTRIBUTES) {
+                String v = attributes.getValue(a);
+                if (v != null) {
+                    priceAttr.put(a, v);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        String value = currVal.toString().trim().replaceAll("\\s+", " ");
+        if ("video".equals(localName)) {
+            if (currAttr != null && currAttr.isValid()) {
+                // add current attribute to attribute list
+                attributes.add(currAttr);
+            }
+            resetCurrent();
+            return;
+        }
+        if (currAttr == null) {
+            currAttr = new VideoAttributes();
+        }
+        if (value.isEmpty()) {
+            // skip value but reset StringBuilder
+        } else if ("thumbnail_loc".equals(localName)) {
+            currAttr.setThumbnailLoc(getURLValue(value));
+        } else if ("title".equals(localName)) {
+            currAttr.setTitle(value);
+        } else if ("description".equals(localName)) {
+            currAttr.setDescription(value);
+        } else if ("content_loc".equals(localName)) {
+            currAttr.setContentLoc(getURLValue(value));
+        } else if ("player_loc".equals(localName)) {
+            currAttr.setPlayerLoc(getURLValue(value));
+        } else if ("duration".equals(localName)) {
+            Integer duration = getIntegerValue(value);
+            if (duration != null && (duration < 0 || duration > 28800)) {
+                LOG.debug("Invalid value for specified duration: {}", duration);
+                duration = null;
+            }
+            currAttr.setDuration(duration);
+        } else if ("expiration_date".equals(localName)) {
+            ZonedDateTime dateTime = SiteMap.convertToZonedDateTime(value);
+            currAttr.setExpirationDate(dateTime);
+        } else if ("rating".equals(localName)) {
+            currAttr.setRating(getFloatValue(value));
+        } else if ("view_count".equals(localName)) {
+            currAttr.setViewCount(getIntegerValue(value));
+        } else if ("publication_date".equals(localName)) {
+            currAttr.setPublicationDate(getDateValue(value));
+        } else if ("family_friendly".equals(localName)) {
+            currAttr.setFamilyFriendly(!"No".equalsIgnoreCase(value));
+        } else if ("tag".equals(localName)) {
+            currAttr.addTag(value);
+        } else if ("category".equals(localName)) {
+            currAttr.setCategory(value);
+        } else if ("restriction".equals(localName)) {
+            if (relationAttr != null) {
+                String[] vals = value.split("\\s+");
+                if ("allow".equalsIgnoreCase(relationAttr)) {
+                    currAttr.setAllowedCountries(vals);
+                } else if ("deny".equalsIgnoreCase(relationAttr)) {
+                    currAttr.setRestrictedCountries(vals);
+                }
+            }
+            relationAttr = null;
+        } else if ("gallery_loc".equals(localName)) {
+            currAttr.setGalleryLoc(getURLValue(value));
+        } else if ("price".equals(localName)) {
+            float fvalue = getFloatValue(value);
+            String currency = null;
+            VideoPriceType type = VideoPriceType.own;
+            VideoPriceResolution resolution = null;
+            if (priceAttr != null) {
+                currency = priceAttr.get("currency").trim();
+                String t = priceAttr.get("type");
+                if (t != null && !t.trim().isEmpty()) {
+                    try {
+                        type = VideoPriceType.valueOf(t.trim());
+                    } catch (IllegalArgumentException e) {
+                        LOG.debug("Illegal value for price type: {}", type);
+                    }
+                }
+                String r = priceAttr.get("resolution");
+                if (r != null && !r.trim().isEmpty()) {
+                    try {
+                        resolution = VideoPriceResolution.valueOf(r.trim());
+                    } catch (IllegalArgumentException e) {
+                        LOG.debug("Illegal value for price resolution: {}", resolution);
+                    }
+                }
+            }
+            VideoPrice price = new VideoPrice(currency, fvalue, type, resolution);
+            currAttr.addPrice(price);
+            priceAttr = null;
+        } else if ("requires_subscription".equals(localName)) {
+            currAttr.setRequiresSubscription(getYesNoBooleanValue(value, localName));
+        } else if ("uploader".equals(localName)) {
+            currAttr.setUploader(value);
+        } else if ("platform".equals(localName)) {
+            if (relationAttr != null) {
+                String[] vals = value.split("\\s+");
+                if ("allow".equalsIgnoreCase(relationAttr)) {
+                    currAttr.setAllowedPlatforms(vals);
+                } else if ("deny".equalsIgnoreCase(relationAttr)) {
+                    currAttr.setRestrictedPlatforms(vals);
+                }
+            }
+            relationAttr = null;
+        } else if ("live".equals(localName)) {
+            currAttr.setLive(getYesNoBooleanValue(value, localName));
+        }
+        // reset StringBuilder
+        currVal = new StringBuilder();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        currVal.append(String.valueOf(ch, start, length));
+    }
+
+    @Override
+    public ExtensionMetadata[] getAttributes() {
+        if (currAttr != null && currAttr.isValid()) {
+            /*
+             * add current element to attribute list, do not reset in case
+             * getAttributes is called during parsing of a sitemap <url> element
+             */
+            attributes.add(currAttr);
+        }
+        return super.getAttributes();
+    }
+
+}


Test patch:
diff --git a/src/main/java/crawlercommons/sitemaps/SiteMapTester.java b/src/main/java/crawlercommons/sitemaps/SiteMapTester.java
index eecb03d..a3f8794 100644
--- a/src/main/java/crawlercommons/sitemaps/SiteMapTester.java
+++ b/src/main/java/crawlercommons/sitemaps/SiteMapTester.java
@@ -44,6 +44,8 @@ public class SiteMapTester {
             LOG.error("Java properties:");
             LOG.error("  sitemap.strictNamespace");
             LOG.error("                  if true sitemaps are required to use the standard namespace URI");
+            LOG.error("  sitemap.extensions");
+            LOG.error("                  if true enable sitemap extension parsing");
         } else {
             URL url = new URL(args[0]);
             String mt = (args.length > 1) ? args[1] : null;
@@ -64,6 +66,11 @@ public class SiteMapTester {
         boolean strictNamespace = new Boolean(System.getProperty("sitemap.strictNamespace"));
         saxParser.setStrictNamespace(strictNamespace);
 
+        boolean enableExtensions = new Boolean(System.getProperty("sitemap.extensions"));
+        if (enableExtensions) {
+            saxParser.enableExtensions();
+        }
+
         AbstractSiteMap sm = null;
         // guesses the mimetype
         if (mt == null || mt.equals("")) {
@@ -80,7 +87,11 @@ public class SiteMapTester {
         } else {
             Collection<SiteMapURL> links = ((SiteMap) sm).getSiteMapUrls();
             for (SiteMapURL smu : links) {
-                LOG.info(smu.getUrl().toString());
+                if (enableExtensions) {
+                    LOG.info(smu.toString());
+                } else {
+                    LOG.info(smu.getUrl().toString());
+                }
             }
         }
     }
diff --git a/src/test/java/crawlercommons/sitemaps/SiteMapParserExtensionTest.java b/src/test/java/crawlercommons/sitemaps/SiteMapParserExtensionTest.java
new file mode 100644
index 0000000..b09ac66
--- /dev/null
+++ b/src/test/java/crawlercommons/sitemaps/SiteMapParserExtensionTest.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright 2018 Crawler-Commons
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package crawlercommons.sitemaps;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import crawlercommons.sitemaps.extension.Extension;
+import crawlercommons.sitemaps.extension.ExtensionMetadata;
+import crawlercommons.sitemaps.extension.ImageAttributes;
+import crawlercommons.sitemaps.extension.LinkAttributes;
+import crawlercommons.sitemaps.extension.MobileAttributes;
+import crawlercommons.sitemaps.extension.NewsAttributes;
+import crawlercommons.sitemaps.extension.VideoAttributes;
+
+@RunWith(JUnit4.class)
+public class SiteMapParserExtensionTest {
+
+    @Test
+    public void testVideosSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.enableExtension(Extension.VIDEO);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/sitemap-videos.xml");
+
+        URL url = new URL("http://www.example.com/sitemap-video.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(1, sm.getSiteMapUrls().size());
+        VideoAttributes expectedVideoAttributes = new VideoAttributes(new URL("http://www.example.com/thumbs/123.jpg"), "Grilling steaks for summer",
+                        "Alkis shows you how to get perfectly done steaks every time", new URL("http://www.example.com/video123.flv"), new URL("http://www.example.com/videoplayer.swf?video=123"));
+        expectedVideoAttributes.setDuration(600);
+        ZonedDateTime dt = ZonedDateTime.parse("2009-11-05T19:20:30+08:00");
+        expectedVideoAttributes.setExpirationDate(dt);
+        dt = ZonedDateTime.parse("2007-11-05T19:20:30+08:00");
+        expectedVideoAttributes.setPublicationDate(dt);
+        expectedVideoAttributes.setRating(4.2f);
+        expectedVideoAttributes.setViewCount(12345);
+        expectedVideoAttributes.setFamilyFriendly(true);
+        expectedVideoAttributes.setTags(new String[] { "sample_tag1", "sample_tag2" });
+        expectedVideoAttributes.setAllowedCountries(new String[] { "IE", "GB", "US", "CA" });
+        expectedVideoAttributes.setGalleryLoc(new URL("http://cooking.example.com"));
+        expectedVideoAttributes.setGalleryTitle("Cooking Videos");
+        expectedVideoAttributes.setPrices(new VideoAttributes.VideoPrice[] { new VideoAttributes.VideoPrice("EUR", 1.99f, VideoAttributes.VideoPriceType.own) });
+        expectedVideoAttributes.setRequiresSubscription(true);
+        expectedVideoAttributes.setUploader("GrillyMcGrillerson");
+        expectedVideoAttributes.setUploaderInfo(new URL("http://www.example.com/users/grillymcgrillerson"));
+        expectedVideoAttributes.setLive(false);
+
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            assertNotNull(su.getAttributesForExtension(Extension.VIDEO));
+            VideoAttributes attr = (VideoAttributes) su.getAttributesForExtension(Extension.VIDEO)[0];
+            assertEquals(expectedVideoAttributes, attr);
+        }
+    }
+
+    @Test
+    public void testImageSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.enableExtension(Extension.IMAGE);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/sitemap-images.xml");
+
+        URL url = new URL("http://www.example.com/sitemap-images.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(1, sm.getSiteMapUrls().size());
+        ImageAttributes imageAttributes1 = new ImageAttributes(new URL("http://example.com/image.jpg"));
+        ImageAttributes imageAttributes2 = new ImageAttributes(new URL("http://example.com/photo.jpg"));
+        imageAttributes2.setCaption("This is the caption.");
+        imageAttributes2.setGeoLocation("Limerick, Ireland");
+        imageAttributes2.setTitle("Example photo shot in Limerick, Ireland");
+        imageAttributes2.setLicense(new URL("https://creativecommons.org/licenses/by/4.0/legalcode"));
+
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            assertNotNull(su.getAttributesForExtension(Extension.IMAGE));
+            ExtensionMetadata[] attrs = su.getAttributesForExtension(Extension.IMAGE);
+            ImageAttributes attr = (ImageAttributes) attrs[0];
+            assertEquals(imageAttributes1, attr);
+            attr = (ImageAttributes) attrs[1];
+            assertEquals(imageAttributes2, attr);
+        }
+    }
+
+    @SuppressWarnings("serial")
+    @Test
+    public void testXHTMLLinksSitemap() throws UnknownFormatException, IOException, MalformedURLException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.enableExtension(Extension.LINKS);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/sitemap-links.xml");
+
+        URL url = new URL("http://www.example.com/sitemap-links.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(3, sm.getSiteMapUrls().size());
+        // all three pages share the same links attributes
+        LinkAttributes[] linkAttributes = new LinkAttributes[] { new LinkAttributes(new URL("http://www.example.com/deutsch/")),
+                        new LinkAttributes(new URL("http://www.example.com/schweiz-deutsch/")), new LinkAttributes(new URL("http://www.example.com/english/")) };
+        linkAttributes[0].setParams(new HashMap<String, String>() {
+            {
+                put("rel", "alternate");
+                put("hreflang", "de");
+            }
+        });
+        linkAttributes[1].setParams(new HashMap<String, String>() {
+            {
+                put("rel", "alternate");
+                put("hreflang", "de-ch");
+            }
+        });
+        linkAttributes[2].setParams(new HashMap<String, String>() {
+            {
+                put("rel", "alternate");
+                put("hreflang", "en");
+            }
+        });
+
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            assertNotNull(su.getAttributesForExtension(Extension.LINKS));
+            ExtensionMetadata[] attrs = su.getAttributesForExtension(Extension.LINKS);
+            assertEquals(linkAttributes.length, attrs.length);
+            for (int i = 0; i < linkAttributes.length; i++) {
+                LinkAttributes attr = (LinkAttributes) attrs[i];
+                assertEquals(linkAttributes[i], attr);
+            }
+        }
+    }
+
+    @Test
+    public void testNewsSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.enableExtension(Extension.NEWS);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/sitemap-news.xml");
+
+        URL url = new URL("http://www.example.org/sitemap-news.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(1, sm.getSiteMapUrls().size());
+        ZonedDateTime dt = ZonedDateTime.parse("2008-11-23T00:00:00+00:00");
+        NewsAttributes expectedNewsAttributes = new NewsAttributes("The Example Times", "en", dt, "Companies A, B in Merger Talks");
+        expectedNewsAttributes.setKeywords(new String[] { "business", "merger", "acquisition", "A", "B" });
+        expectedNewsAttributes.setGenres(new NewsAttributes.NewsGenre[] { NewsAttributes.NewsGenre.PressRelease, NewsAttributes.NewsGenre.Blog });
+        expectedNewsAttributes.setStockTickers(new String[] { "NASDAQ:A", "NASDAQ:B" });
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            assertNotNull(su.getAttributesForExtension(Extension.NEWS));
+            NewsAttributes attr = (NewsAttributes) su.getAttributesForExtension(Extension.NEWS)[0];
+            assertEquals(expectedNewsAttributes, attr);
+        }
+    }
+
+    @Test
+    public void testMobileSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.enableExtension(Extension.MOBILE);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/sitemap-mobile.xml");
+
+        URL url = new URL("http://www.example.org/sitemap-mobile.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            URL u = su.getUrl();
+            ExtensionMetadata[] attrs = su.getAttributesForExtension(Extension.MOBILE);
+            if (u.getPath().contains("mobile-friendly")) {
+                assertNotNull(attrs);
+                MobileAttributes attr = (MobileAttributes) attrs[0];
+                assertNotNull(attr);
+            } else {
+                assertTrue(attrs == null || attrs.length == 0);
+            }
+        }
+    }
+
+    @Test
+    public void testShinpaideshuNewsSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.setStrictNamespace(true);
+        parser.enableExtension(Extension.NEWS);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/shinpaideshou-news-sitemap.xml");
+
+        URL url = new URL("https://shinpaideshou.wordpress.com/news-sitemap.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(3, sm.getSiteMapUrls().size());
+        for (SiteMapURL su : sm.getSiteMapUrls()) {
+            assertNotNull(su.getAttributesForExtension(Extension.NEWS));
+            NewsAttributes attr = (NewsAttributes) su.getAttributesForExtension(Extension.NEWS)[0];
+            assertNotNull(attr.getName());
+            assertNotNull(attr.getPublicationDateTime());
+            assertEquals(2017, attr.getPublicationDateTime().getYear());
+        }
+    }
+
+    @Test
+    public void testHebdenbridgetimesArticlesSitemap() throws UnknownFormatException, IOException {
+        SiteMapParser parser = new SiteMapParser();
+        parser.setStrictNamespace(true);
+        parser.enableExtension(Extension.NEWS);
+        parser.enableExtension(Extension.IMAGE);
+        parser.enableExtension(Extension.VIDEO);
+        parser.enableExtension(Extension.MOBILE);
+
+        String contentType = "text/xml";
+        byte[] content = SiteMapParserTest.getResourceAsBytes("src/test/resources/sitemaps/extension/hebdenbridgetimes-articles-sitemap.xml");
+
+        URL url = new URL("http://www.hebdenbridgetimes.co.uk/sitemap-article-2015-18.xml");
+        AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
+        assertEquals(false, asm.isIndex());
+        assertEquals(true, asm instanceof SiteMap);
+        SiteMap sm = (SiteMap) asm;
+        assertEquals(74, sm.getSiteMapUrls().size());
+    }
+
+}
diff --git a/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java b/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
index c680637..10fc53c 100644
--- a/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
+++ b/src/test/java/crawlercommons/sitemaps/SiteMapParserTest.java
@@ -292,7 +292,7 @@ public class SiteMapParserTest {
     public void testSitemapTextGZ() throws UnknownFormatException, IOException {
         SiteMapParser parser = new SiteMapParser();
         String contentType = "application/gzip";
-        byte[] content = this.getResourceAsBytes("src/test/resources/sitemaps/sitemap.txt.gz");
+        byte[] content = getResourceAsBytes("src/test/resources/sitemaps/sitemap.txt.gz");
 
         URL url = new URL("http://www.example.com/sitemap.txt.gz");
         AbstractSiteMap asm = parser.parseSiteMap(contentType, content, url);
@@ -590,7 +590,7 @@ public class SiteMapParserTest {
      * @return byte content of the file
      * @throws IOException
      */
-    private byte[] getResourceAsBytes(String resourceName) throws IOException {
+    protected static byte[] getResourceAsBytes(String resourceName) throws IOException {
         File file = new File(resourceName);
         InputStream is = new FileInputStream(file);
         return IOUtils.toByteArray(is);
diff --git a/src/test/resources/sitemaps/extension/hebdenbridgetimes-articles-sitemap.xml b/src/test/resources/sitemaps/extension/hebdenbridgetimes-articles-sitemap.xml
new file mode 100644
index 0000000..5dfde34
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/hebdenbridgetimes-articles-sitemap.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" >
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/video-tour-de-yorkshire-in-calderdale-1-7242580</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T18:51:50+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242578!/image/417104257.jpg_gen/derivatives/landscape_595/417104257.jpg</image:loc><image:caption>Tour De Yorkshire passes through Nutclough, Hebden Bridge</image:caption><image:title>Tour De Yorkshire passes through Nutclough, Hebden Bridge</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242578!/image/417104257.jpg_gen/derivatives/box_100/417104257.jpg</video:thumbnail_loc>
+<video:title>VIDEO - Tour de Yorkshire in Calderdale</video:title>
+<video:description>&lt;!--PSTYLE=WINT Web Intro--&gt;&lt;p&gt;Thousands of people turned out to watch the Tour de Yorkshire arrive in Calderdale.&lt;/p&gt;</video:description>
+<video:publication_date>2015-05-03T18:44:05+01:00</video:publication_date>
+<video:tag>Carousels</video:tag>
+<video:tag>Local</video:tag>
+<video:category>Carousels</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/concern-for-missing-todmorden-man-1-7242564</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T18:21:08+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242403!/image/1361572363.jpg_gen/derivatives/landscape_595/1361572363.jpg</image:loc><image:caption>Missing man Stephen Ellis</image:caption><image:title>Missing man Stephen Ellis</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/fax-ensure-donny-never-at-races-1-7242483</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T17:05:02+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/tour-de-france/tour-de-yorkshire-hermans-wins-into-leeds-as-nordhaug-claims-jersey-1-7242482</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T18:43:23+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242481!/image/3155906199.jpg_gen/derivatives/landscape_595/3155906199.jpg</image:loc><image:caption>The break away group makes its way up past Baitlings Reservoir on the way out of Ripponden. (Picture: Scott Merrylees SM1008/31b)</image:caption><image:title>The break away group makes its way up past Baitlings Reservoir on the way out of Ripponden. (Picture: Scott Merrylees SM1008/31b)</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/train-lines-now-clear-following-incident-between-todmorden-and-rochdale-1-7242431</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T16:05:09+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7076397!/image/481865146.jpg_gen/derivatives/landscape_595/481865146.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/cyclist-taken-to-hospital-after-crash-on-tour-de-yorkshire-route-1-7242335</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T14:44:19+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6507643!/image/2232453533.jpg_gen/derivatives/landscape_595/2232453533.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/party-atmosphere-created-in-wakefield-for-tour-de-yorkshire-1-7242312</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T14:14:51+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242309!/image/2853568059.jpg_gen/derivatives/landscape_595/2853568059.jpg</image:loc><image:caption>Tour de Yorkshire ceremonial start in Wakefield city centre</image:caption><image:title>Tour de Yorkshire ceremonial start in Wakefield city centre</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242309!/image/2853568059.jpg_gen/derivatives/box_100/2853568059.jpg</video:thumbnail_loc>
+<video:title>Party atmosphere created in Wakefield for Tour de Yorkshire</video:title>
+<video:description>&lt;!--PSTYLE=WINT Web Intro--&gt;&lt;p&gt;A party atmosphere was created in Wakefield for the start of final day of the inaugural Tour de Yorkshire.&lt;/p&gt;</video:description>
+<video:publication_date>2015-05-03T14:15:09+01:00</video:publication_date>
+<video:tag>Carousels</video:tag>
+<video:tag>Local</video:tag>
+<video:tag>Regional</video:tag>
+<video:tag>cycling</video:tag>
+<video:tag>human interest</video:tag>
+<video:category>Carousels</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/league-new-boys-get-off-mark-1-7242226</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T12:29:56+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242225!/image/3755567044.jpg_gen/derivatives/landscape_595/3755567044.jpg</image:loc><image:caption>Actions from the Parish Cup quarter-final, Triangle v Jer Lane at Triangle.Pictured is Nasa Chowdrey</image:caption><image:title>Actions from the Parish Cup quarter-final, Triangle v Jer Lane at Triangle.Pictured is Nasa Chowdrey</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/tour-de-yorkshire-to-make-history-in-wakefield-1-7242189</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T13:59:35+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242244!/image/3101819633.jpg_gen/derivatives/landscape_595/3101819633.jpg</image:loc><image:caption>Riders leave the start of the third stage of the Tour de Yorkshire between Wakefield and Leeds. PIC: PA</image:caption><image:title>Riders leave the start of the third stage of the Tour de Yorkshire between Wakefield and Leeds. PIC: PA</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242184!/image/3547624603.jpg_gen/derivatives/landscape_595/3547624603.jpg</image:loc></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242185!/image/2368897871.jpg_gen/derivatives/landscape_595/2368897871.jpg</image:loc></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242187!/image/1733470752.jpg_gen/derivatives/landscape_595/1733470752.jpg</image:loc><image:caption>Tour de Yorkshire full route</image:caption><image:title>Tour de Yorkshire full route</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/elland-overhauled-in-second-half-1-7242115</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T10:27:38+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242114!/image/1058690779.jpg_gen/derivatives/landscape_595/1058690779.jpg</image:loc><image:caption>Elland v Normanton KnightsChris Dyson</image:caption><image:title>Elland v Normanton KnightsChris Dyson</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/bradbury-double-helps-illingworth-lift-cup-1-7242100</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T10:02:42+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242099!/image/3797926460.jpg_gen/derivatives/landscape_595/3797926460.jpg</image:loc><image:caption>Illingworth celebrate their fee Halifax Cup final success</image:caption><image:title>Illingworth celebrate their fee Halifax Cup final success</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/business/business-news/your-guide-to-bus-service-changes-in-calderdale-as-tour-de-yorkshire-arrives-1-7242094</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T10:01:51+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7168687!/image/2137476.jpg_gen/derivatives/landscape_595/2137476.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/the-wait-is-finally-over-as-tour-de-yorkshire-arrives-in-calderdale-1-7242061</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-03T11:30:36+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7242060!/image/3287702651.jpg_gen/derivatives/landscape_595/3287702651.jpg</image:loc><image:caption>Tour de France. Commercial Street, Hebden Bridge.</image:caption><image:title>Tour de France. Commercial Street, Hebden Bridge.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/lifestyle/a-top-double-bill-of-laughter-1-7241897</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T18:29:09+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241896!/image/1611288434.jpg_gen/derivatives/landscape_595/1611288434.jpg</image:loc><image:caption>Rob Rouse</image:caption><image:title>Rob Rouse</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/reduce-reuse-and-recycle-message-of-alternatiba-festival-on-sunday-1-7241801</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T17:16:59+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/midgley-s-young-side-looking-to-upset-odds-at-the-shay-1-7237270</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T11:54:08+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7237269!/image/2020168207.jpg_gen/derivatives/landscape_595/2020168207.jpg</image:loc><image:caption>Darren Nelson will lead Midgley Reserves out at the Shay</image:caption><image:title>Darren Nelson will lead Midgley Reserves out at the Shay</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/lifestyle/walking-link-has-national-profile-1-7241764</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T16:48:39+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241763!/image/1705175353.jpg_gen/derivatives/landscape_595/1705175353.jpg</image:loc><image:caption>Walkers on their way, pictured  by Jim Fitton</image:caption><image:title>Walkers on their way, pictured  by Jim Fitton</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/coroner-verdict-in-student-s-death-1-7241761</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T16:44:02+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/lend-us-a-hand-with-your-great-teamwork-1-7241749</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T16:27:27+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241748!/image/2280555825.jpg_gen/derivatives/landscape_595/2280555825.jpg</image:loc><image:caption>A turtle pictured at a previous parade, by Craig Shaw/blu planet photography</image:caption><image:title>A turtle pictured at a previous parade, by Craig Shaw/blu planet photography</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/tour-de-yorkshire-in-calderdale-bus-delays-expected-1-7241680</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T15:17:23+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7168687!/image/2137476.jpg_gen/derivatives/landscape_595/2137476.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/what-s-on/arts-culture-and-entertainment/wet-your-whistle-at-hebden-bridge-beer-and-cider-festival-1-7241363</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T10:19:59+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241362!/image/3282720330.jpg_gen/derivatives/landscape_595/3282720330.jpg</image:loc><image:caption>Organisers Martin Ogley, Amy Leader, Phil Kennedy and Sue Cooper.</image:caption><image:title>Organisers Martin Ogley, Amy Leader, Phil Kennedy and Sue Cooper.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/police-appeal-for-information-after-distinctive-lorry-is-stolen-in-heptonstall-1-7241282</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T08:32:07+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241281!/image/1277918547.jpg_gen/derivatives/landscape_595/1277918547.jpg</image:loc><image:caption>Ford Transit lorry stolen in Heptonstall</image:caption><image:title>Ford Transit lorry stolen in Heptonstall</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/a-top-walk-or-hike-1-7241026</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T19:53:35+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/a-big-race-weekend-welcome-to-all-tour-de-yorkshire-and-more-comes-to-cragg-vale-mytholmroyd-and-hebden-bridge-1-7241035</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T20:07:55+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7241034!/image/3112145229.jpg_gen/derivatives/landscape_595/3112145229.jpg</image:loc><image:caption>The weekend starts here! Ready for Cragg Fest and the Tour de Yorkshire at the Robin Hood Inn, Cragg Vale, with organiser Joanne Hull, fourth from left, and landlady Elva Wood, far righ</image:caption><image:title>The weekend starts here! Ready for Cragg Fest and the Tour de Yorkshire at the Robin Hood Inn, Cragg Vale, with organiser Joanne Hull, fourth from left, and landlady Elva Wood, far righ</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/may-service-at-the-praying-hole-1-7241028</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T19:58:52+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/tv-star-sam-ready-for-big-fun-day-out-1-7241025</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T19:52:00+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/cockcroft-edges-out-allott-in-medal-1-7238239</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T16:26:28+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/video-stage-one-of-tour-de-yorkshire-1-7240857</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T17:59:47+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7240855!/image/3354952474.jpg_gen/derivatives/landscape_595/3354952474.jpg</image:loc><image:caption>TOUR DE YORKSHIRE 2015 --- Dalby Forest Section on Stage 1 of the first Tour De Yorkshire bike race. // Riders and spectators on the Cote de Dalby. Friday 1st May 2015. HARRY ATKINSON REF:</image:caption><image:title>TOUR DE YORKSHIRE 2015 --- Dalby Forest Section on Stage 1 of the first Tour De Yorkshire bike race. // Riders and spectators on the Cote de Dalby. Friday 1st May 2015. HARRY ATKINSON REF:</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7240855!/image/3354952474.jpg_gen/derivatives/box_100/3354952474.jpg</video:thumbnail_loc>
+<video:title>VIDEO: Stage one of Tour De Yorkshire</video:title>
+<video:description>&lt;!--PSTYLE=WINT Web Intro--&gt;&lt;p&gt;Stage one of the first ever Tour de Yorkshire has been a huge success.&lt;/p&gt;</video:description>
+<video:publication_date>2015-05-01T17:39:49+01:00</video:publication_date>
+<video:tag>Local</video:tag>
+<video:tag>cycling</video:tag>
+<video:category>Local</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/crickettalk-yorkshire-making-steady-progress-in-defence-of-title-1-7105637</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T14:59:34+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7240206!/image/3361429185.jpg_gen/derivatives/landscape_595/3361429185.jpg</image:loc><image:caption>Yorkshire&apos;s Andrew Gale celebrates his 50 against Warwickshire. The CricketTalk panel discuss.</image:caption><image:title>Yorkshire&apos;s Andrew Gale celebrates his 50 against Warwickshire. The CricketTalk panel discuss.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/tour-crowds-flock-to-see-riders-1-7239909</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T17:42:15+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239905!/image/1681765054.jpg_gen/derivatives/landscape_595/1681765054.jpg</image:loc></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239906!/image/3171490448.jpg_gen/derivatives/landscape_595/3171490448.jpg</image:loc></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239907!/image/2325189868.jpg_gen/derivatives/landscape_595/2325189868.jpg</image:loc></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239908!/image/469695493.jpg_gen/derivatives/landscape_595/469695493.jpg</image:loc><image:caption>Tour de Yorkshire . The bikes race through Cayton. pic Richard Ponter 151725a</image:caption><image:title>Tour de Yorkshire . The bikes race through Cayton. pic Richard Ponter 151725a</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239905!/image/1681765054.jpg_gen/derivatives/box_100/1681765054.jpg</video:thumbnail_loc>
+<video:title>Tour: Crowds flock to see riders</video:title>
+<video:description>&lt;!--PSTYLE=WINT Web Intro--&gt;&lt;p&gt;In Seamer, famous for its annual horse fair, the locals turned out in force to show that it was not a case of four legs good, two wheels bad.&lt;/p&gt;</video:description>
+<video:publication_date>2015-05-01T13:34:07+01:00</video:publication_date>
+<video:tag>Local</video:tag>
+<video:tag>cycling</video:tag>
+<video:category>Local</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/tour-racing-ahead-to-calderdale-1-7239866</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T13:25:04+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7239865!/image/79521157.jpg_gen/derivatives/landscape_595/79521157.jpg</image:loc><image:caption>Richard Palliser with giant Yorkshire Rose on his house.</image:caption><image:title>Richard Palliser with giant Yorkshire Rose on his house.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/sky-s-the-limit-for-todmorden-brothers-1-7234763</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T14:04:09+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234757!/image/832204628.jpg_gen/derivatives/landscape_595/832204628.jpg</image:loc><image:caption>Nathan, centre, with brother Andrew, parents Joanne and Eldred, left, and volunteers from Calder Valley Search and Rescue Team at Stoodley Pike</image:caption><image:title>Nathan, centre, with brother Andrew, parents Joanne and Eldred, left, and volunteers from Calder Valley Search and Rescue Team at Stoodley Pike</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234758!/image/1228031543.jpg_gen/derivatives/landscape_595/1228031543.jpg</image:loc><image:caption>Nathan with Calder Valley Search and Rescue Team members with Stoodley Pike monument in the background</image:caption><image:title>Nathan with Calder Valley Search and Rescue Team members with Stoodley Pike monument in the background</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234759!/image/217728740.jpg_gen/derivatives/landscape_595/217728740.jpg</image:loc><image:caption>Andrew being carried to Stoodley Pike viewing platform</image:caption><image:title>Andrew being carried to Stoodley Pike viewing platform</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234760!/image/1538683293.jpg_gen/derivatives/landscape_595/1538683293.jpg</image:loc><image:caption>Andrew Greenwood, 14, being hoisted to the top of Stoodley Pike, Todmorden</image:caption><image:title>Andrew Greenwood, 14, being hoisted to the top of Stoodley Pike, Todmorden</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234761!/image/3372919781.jpg_gen/derivatives/landscape_595/3372919781.jpg</image:loc><image:caption>Aim Higher</image:caption><image:title>Aim Higher</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234757!/image/832204628.jpg_gen/derivatives/box_100/832204628.jpg</video:thumbnail_loc>
+<video:title>Sky&#8217;s the limit for Todmorden brothers</video:title>
+<video:description>&lt;!--PSTYLE=WINT_Web Intro--&gt;&lt;p&gt;A dream came true for a Todmorden family when two disabled brothers were hoisted to the top of Stoodley Pike.&lt;/p&gt;</video:description>
+<video:publication_date>2015-05-01T12:00:00+01:00</video:publication_date>
+<video:tag>Campaigns</video:tag>
+<video:tag>Carousels</video:tag>
+<video:tag>Community</video:tag>
+<video:tag>children</video:tag>
+<video:tag>disabled</video:tag>
+<video:tag>high schools (Secondary)</video:tag>
+<video:tag>parent and child</video:tag>
+<video:category>Campaigns</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/ex-police-chief-norman-bettison-my-regret-at-liverpool-fans-slur-1-7239657</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-02T10:22:02+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236587!/image/3059536438.jpg_gen/derivatives/landscape_595/3059536438.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236586!/image/1138521334.jpg_gen/derivatives/landscape_595/1138521334.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236588!/image/593604228.jpg_gen/derivatives/landscape_595/593604228.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/labour-launches-scheme-for-first-time-buyers-1-7236912</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T09:54:11+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236910!/image/712812938.jpg_gen/derivatives/landscape_595/712812938.jpg</image:loc><image:caption>Hilary Benn and Josh Fenton-Glynn meet first time home buyers Danny and Sarah at Hove Edge.</image:caption><image:title>Hilary Benn and Josh Fenton-Glynn meet first time home buyers Danny and Sarah at Hove Edge.</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236911!/image/2793406719.jpg_gen/derivatives/landscape_595/2793406719.jpg</image:loc><image:caption>Hilary Benn and Josh Fenton-Glynn meet first time home buyers Danny and Sarah at Hove Edge.</image:caption><image:title>Hilary Benn and Josh Fenton-Glynn meet first time home buyers Danny and Sarah at Hove Edge.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/business/business-news/calderdale-set-for-a-dry-and-sunny-day-1-6778451</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T08:50:35+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5870706!/image/3739992826.jpg_gen/derivatives/landscape_595/3739992826.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/thieves-take-war-medal-and-pistol-in-todmorden-raid-1-7238977</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T08:34:14+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7238976!/image/3202812228.jpg_gen/derivatives/landscape_595/3202812228.jpg</image:loc><image:caption>A World War One service medal similar to the one stolen in Todmorden  between Thursday, April 16, and Sunday,  April 19. The medal stolen in Todmorden did not have a ribbon but was engraved with &quot;H.Mitchell&quot; and is silver with a bar attached.</image:caption><image:title>A World War One service medal similar to the one stolen in Todmorden  between Thursday, April 16, and Sunday,  April 19. The medal stolen in Todmorden did not have a ribbon but was engraved with &quot;H.Mitchell&quot; and is silver with a bar attached.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/borough-unable-to-roll-over-stone-in-possible-west-lancs-swansong-1-7237626</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T13:34:29+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7237625!/image/3669995649.jpg_gen/derivatives/landscape_595/3669995649.jpg</image:loc><image:caption>Boroughs Chris Kay (left) closes down an opponent on Saturday</image:caption><image:title>Boroughs Chris Kay (left) closes down an opponent on Saturday</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/ex-police-chief-norman-bettison-plotted-in-pub-to-blame-liverpool-fans-for-hillsborough-disaster-1-7236590</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T11:47:15+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236586!/image/1138521334.jpg_gen/derivatives/landscape_595/1138521334.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236587!/image/3059536438.jpg_gen/derivatives/landscape_595/3059536438.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236588!/image/593604228.jpg_gen/derivatives/landscape_595/593604228.jpg</image:loc><image:caption>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:caption><image:title>Former Chief Constable Sir Norman Bettison arrives to give evidence at the Hillsborough inquest in Warrington, on his role on the day of the 1989 FA Cup semi-final tragedy and in gathering police evidence for the Taylor Inquiry</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/calder-storm-up-to-third-place-with-comfortable-derby-victory-over-star-1-7238238</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T16:26:28+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/bannisters-win-for-district-side-1-7238231</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T16:25:31+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/tour-de-france/tour-gets-in-gear-1-7238007</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T15:27:37+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7238006!/image/2422472578.jpg_gen/derivatives/landscape_595/2422472578.jpg</image:loc><image:caption>Opening of the Tour de Yorkshire Media Centre  at York Racecourse, pictured with the winners trophy are Gary Verity, Chief Executive of Welcome to Yorkshire, Bernard Hinault  5 times winner of the Tour de France, and Christian Prudhomme: Director of Tour de France, ASO.  30 April 2015.  Picture Bruce Rollinson</image:caption><image:title>Opening of the Tour de Yorkshire Media Centre  at York Racecourse, pictured with the winners trophy are Gary Verity, Chief Executive of Welcome to Yorkshire, Bernard Hinault  5 times winner of the Tour de France, and Christian Prudhomme: Director of Tour de France, ASO.  30 April 2015.  Picture Bruce Rollinson</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/region-s-police-confident-160k-tour-de-france-overtime-bill-will-not-be-repeated-1-7237421</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T12:22:32+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7237419!/image/3781390213.jpg_gen/derivatives/landscape_595/3781390213.jpg</image:loc><image:caption>Newspaper: Wakefield Express.Story: Acting Chief Constable Mark Milsom. Media talk about policing the Tour de Yorkshire.Photo Date: 21/04/15Photo ref: AB001b0415</image:caption><image:title>Newspaper: Wakefield Express.Story: Acting Chief Constable Mark Milsom. Media talk about policing the Tour de Yorkshire.Photo Date: 21/04/15Photo ref: AB001b0415</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/royd-club-hit-six-to-save-their-skins-1-7237216</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T11:45:34+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7237215!/image/909679034.jpg_gen/derivatives/landscape_595/909679034.jpg</image:loc><image:caption>Sam Taylor (right) led Royd Clubs successful survival bid with a hat-trick on Sunday</image:caption><image:title>Sam Taylor (right) led Royd Clubs successful survival bid with a hat-trick on Sunday</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/man-in-critical-condition-following-m62-crash-1-7235484</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T11:34:41+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7237142!/image/2566588130.jpg_gen/derivatives/landscape_595/2566588130.jpg</image:loc><image:caption>The wrecked HGV on the M62</image:caption><image:title>The wrecked HGV on the M62</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7235614!/image/3561521233.jpg_gen/derivatives/landscape_595/3561521233.jpg</image:loc><image:caption>Queues on the M62 viewed from Scammanden Bridge. The outside lanes in both directions are closed between junction 22 (Rishworth Moor) and junction 24 (Ainley Top) following a crash involving two HGVs</image:caption><image:title>Queues on the M62 viewed from Scammanden Bridge. The outside lanes in both directions are closed between junction 22 (Rishworth Moor) and junction 24 (Ainley Top) following a crash involving two HGVs</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/lifestyle/celebrating-a-special-day-1-7237046</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T11:18:31+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6849509!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/yorkshire-first-calls-for-regional-devolution-across-england-1-7236962</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T11:03:21+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236961!/image/703755376.jpg_gen/derivatives/landscape_595/703755376.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/what-s-on/arts-culture-and-entertainment/poetry-tonight-april-30-7pm-with-a-difference-as-cancer-sufferer-steve-raises-money-for-macmillan-1-7236886</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T10:48:58+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236885!/image/741941993.jpg_gen/derivatives/landscape_595/741941993.jpg</image:loc><image:caption>Steve &apos;Manifold&apos; Anderson</image:caption><image:title>Steve &apos;Manifold&apos; Anderson</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/tour-de-france/download-our-24-page-guide-to-the-tour-de-yorkshire-1-7236683</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T10:22:40+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7229582!/image/3067171134.jpg_gen/derivatives/landscape_595/3067171134.jpg</image:loc><image:caption>Fans cheer on cyclists as they tackle the Holme Moss climb during the Tour de France in Yoorkshire last summer. Picture by Simon Hulme</image:caption><image:title>Fans cheer on cyclists as they tackle the Holme Moss climb during the Tour de France in Yoorkshire last summer. Picture by Simon Hulme</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/police-want-to-talk-to-these-people-about-robbery-and-thefts-in-calderdale-1-7236667</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T16:24:21+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236728!/image/2574503092.jpg_gen/derivatives/landscape_595/2574503092.jpg</image:loc><image:caption>Caught on camera - CD467</image:caption><image:title>Caught on camera - CD467</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236729!/image/2813311441.jpg_gen/derivatives/landscape_595/2813311441.jpg</image:loc><image:caption>Caught on camera - CD473</image:caption><image:title>Caught on camera - CD473</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236730!/image/3715099313.jpg_gen/derivatives/landscape_595/3715099313.jpg</image:loc><image:caption>Caught on camera - CD471</image:caption><image:title>Caught on camera - CD471</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236731!/image/641954517.jpg_gen/derivatives/landscape_595/641954517.jpg</image:loc><image:caption>Caught on camera - CD469</image:caption><image:title>Caught on camera - CD469</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236732!/image/3759129345.jpg_gen/derivatives/landscape_595/3759129345.jpg</image:loc><image:caption>Caught on camera - CD470</image:caption><image:title>Caught on camera - CD470</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236733!/image/455301989.jpg_gen/derivatives/landscape_595/455301989.jpg</image:loc><image:caption>Caught on camera - CD468</image:caption><image:title>Caught on camera - CD468</image:title></image:image><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7236735!/image/361719233.jpg_gen/derivatives/landscape_595/361719233.jpg</image:loc><image:caption>Caught on camera - CD474</image:caption><image:title>Caught on camera - CD474</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/big-wins-for-illingworth-and-mixenden-1-7236452</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-30T08:38:37+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/county-championship-yorkshire-hold-on-for-headingley-draw-1-7229873</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T18:44:24+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7232038!/image/619207434.jpg_gen/derivatives/landscape_595/619207434.jpg</image:loc><image:caption>UNDER PRESSURE: Yorkshire captain Andrew Gale hits out. Picture: SWPIX.COM</image:caption><image:title>UNDER PRESSURE: Yorkshire captain Andrew Gale hits out. Picture: SWPIX.COM</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/ukip-candidate-says-a-hung-parliament-is-more-democratic-1-7235572</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T15:38:57+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/man-injured-in-hebden-bridge-gang-attack-1-7234349</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T10:53:51+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234348!/image/4204260754.jpg_gen/derivatives/landscape_595/4204260754.jpg</image:loc><image:caption>Carlton Street, Hebden Bridge</image:caption><image:title>Carlton Street, Hebden Bridge</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/warning-over-alarming-variation-in-cancer-survival-1-7234462</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T09:20:30+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234461!/image/301291714.jpg_gen/derivatives/landscape_595/301291714.jpg</image:loc><image:caption>Picture: PA Wire</image:caption><image:title>Picture: PA Wire</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/captain-gale-takes-umpire-s-error-on-the-chin-1-7234160</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-29T09:17:00+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7234159!/image/3361429185.jpg_gen/derivatives/landscape_595/3361429185.jpg</image:loc><image:caption>Yorkshire&apos;s Andrew Gale celebrates his 50 against Warwickshire.</image:caption><image:title>Yorkshire&apos;s Andrew Gale celebrates his 50 against Warwickshire.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/business/property-news/property-of-the-week-your-bed-of-roses-1-7229770</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T14:54:31+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7229769!/image/1806889583.jpg_gen/derivatives/landscape_595/1806889583.jpg</image:loc><image:caption>Nettlebed Farm, Colden, Hebden Bridge</image:caption><image:title>Nettlebed Farm, Colden, Hebden Bridge</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/man-accused-of-touching-girl-said-she-had-ants-in-knickers-1-7233481</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-28T15:42:21+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7129689!/image/1157290453.jpg_gen/derivatives/landscape_595/1157290453.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/stump-are-halifax-afl-champions-1-7232981</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-28T12:18:50+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.5906109!/image/2921519132.jpg_gen/derivatives/landscape_595/2921519132.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/movement-help-sow-the-seeds-of-success-1-7190074</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-02T12:17:16+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7190072!/image/1621224548.jpg_gen/derivatives/landscape_595/1621224548.jpg</image:loc><image:caption>Pam Warhurst and Mary Clear of IET</image:caption><image:title>Pam Warhurst and Mary Clear of IET</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/video-owners-fit-wheels-to-pet-tortoise-1-7232576</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-28T11:07:47+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7232575!/image/4068736739.jpg_gen/derivatives/landscape_595/4068736739.jpg</image:loc><image:caption>Mrs T, the tortoise on wheels. Picture: Ross Parry Agency</image:caption><image:title>Mrs T, the tortoise on wheels. Picture: Ross Parry Agency</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/beetle-mania-in-car-auction-1-7232560</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-05-01T08:40:25+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7232558!/image/934335677.jpg_gen/derivatives/landscape_595/934335677.jpg</image:loc><image:caption>Richard Temple of Eddisons and VW beetle collector Rolf Gardener.</image:caption><image:title>Richard Temple of Eddisons and VW beetle collector Rolf Gardener.</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/man-rescued-after-crashing-into-tree-in-todmorden-1-7232509</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-28T09:34:52+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7232508!/image/149522432.jpg_gen/derivatives/landscape_595/149522432.jpg</image:loc><image:caption>A man was airlifted to hospital after crashing into a tree in Todmorden. He was rescued by members of Calder Valley Search and Rescue Team</image:caption><image:title>A man was airlifted to hospital after crashing into a tree in Todmorden. He was rescued by members of Calder Valley Search and Rescue Team</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/it-ll-be-a-storm-in-a-pint-pot-1-7229924</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T17:40:47+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/walk-mirrors-their-journey-1-7229963</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T18:03:52+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7229962!/image/4249323721.jpg_gen/derivatives/landscape_595/4249323721.jpg</image:loc><image:caption>Kate Moreton-Deakin</image:caption><image:title>Kate Moreton-Deakin</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/topics-festival-will-showcase-in-may-1-7229946</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T17:55:55+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.6865138!/image/1052246790.jpg_gen/derivatives/landscape_595/1052246790.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/labour-rage-at-future-threat-to-hospital-1-7215322</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-17T12:17:57+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7215321!/image/1805362716.jpg_gen/derivatives/landscape_595/1805362716.jpg</image:loc><image:caption>Labour Parliamentary candidates Holly Lynch and Josh Fenton-Glynn, right, with Labour leader coun Tim Swift outside Calderdale Royal A and E</image:caption><image:title>Labour Parliamentary candidates Holly Lynch and Josh Fenton-Glynn, right, with Labour leader coun Tim Swift outside Calderdale Royal A and E</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/video-disabled-brothers-make-history-and-the-top-of-stoodley-pike-1-7231223</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T13:52:02+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7231221!/image/2552534466.jpg_gen/derivatives/landscape_595/2552534466.jpg</image:loc><image:caption>Calder Valley Search and Rescue Team transport disabled brothers Andrew and Nathan to the top of Stoodley Pike, Todmorden</image:caption><image:title>Calder Valley Search and Rescue Team transport disabled brothers Andrew and Nathan to the top of Stoodley Pike, Todmorden</image:title></image:image><video:video>
+<video:player_loc>http://c.brightcove.com/services/viewer/federated_f9?playerID=3266758341001&amp;playerKey=AQ~~,AAAAAH7YAAk~,wy3kE_WV5OlvkQr9bqeNF6u7R1L-_Iuf&amp;isVid=true&amp;isUI=true&amp;videoID=$VIDEO_ID</video:player_loc>
+<video:thumbnail_loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7231221!/image/2552534466.jpg_gen/derivatives/box_100/2552534466.jpg</video:thumbnail_loc>
+<video:title>Video: Disabled brothers make history - and the top of Stoodley Pike</video:title>
+<video:description>&lt;!--PSTYLE=WINT_Web Intro--&gt;&lt;p&gt;Two brothers, whose condtion means they are permanent wheelchair users, were rope lifted to the viewing platform of one of Calderdale&amp;#8217;s most iconic landmarks. &lt;/p&gt;</video:description>
+<video:publication_date>2015-04-27T13:52:15+01:00</video:publication_date>
+<video:tag>Carousels</video:tag>
+<video:tag>Community</video:tag>
+<video:tag>accomplishment</video:tag>
+<video:tag>disabled</video:tag>
+<video:tag>high schools (Secondary)</video:tag>
+<video:category>Carousels</video:category>
+</video:video>
+</url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/celebrations-latest-chapter-in-ongoing-story-of-stoodley-pike-1-7229771</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T14:55:48+01:00</lastmod></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/victory-will-give-stump-the-title-1-7230627</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T09:52:56+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7230626!/image/235940980.jpg_gen/derivatives/landscape_595/235940980.jpg</image:loc><image:caption>Stump Cross v Calder76. Ben Hiley and Gav Flemming</image:caption><image:title>Stump Cross v Calder76. Ben Hiley and Gav Flemming</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/repeat-bails-for-dozens-of-west-yorkshire-suspects-1-7230576</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T09:24:57+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7230575!/image/1100123077.jpg_gen/derivatives/landscape_595/1100123077.jpg</image:loc><image:caption>Chief Superintendent Chris Hardern</image:caption><image:title>Chief Superintendent Chris Hardern</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/mobile-speed-cameras-around-calderdale-this-week-1-7230484</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T08:45:55+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7230482!/image/1169478591.jpg_gen/derivatives/landscape_595/1169478591.jpg</image:loc></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/ytyytyytrytrytryttrytrytyttrytr-1-7230475</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T08:43:28+01:00</lastmod></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/sport/local-sport/barkisland-take-derby-spoils-1-7230469</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-27T08:36:59+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7230468!/image/423915323.jpg_gen/derivatives/landscape_595/423915323.jpg</image:loc><image:caption>Cricket - Parish Cup final between SBCI and Mytholmroyd at Blackley CCAlex Kaye (right) celebrates his century with batting partner Simod Wood</image:caption><image:title>Cricket - Parish Cup final between SBCI and Mytholmroyd at Blackley CCAlex Kaye (right) celebrates his century with batting partner Simod Wood</image:title></image:image></url>
+<url><loc>http://www.hebdenbridgetimes.co.uk/news/local/now-you-see-it-now-you-don-t-it-s-off-the-wall-1-7229940</loc><mobile:mobile/><changefreq>daily</changefreq><lastmod>2015-04-26T17:47:55+01:00</lastmod><image:image><image:loc>http://www.hebdenbridgetimes.co.uk/webimage/1.7229939!/image/632565411.jpg_gen/derivatives/landscape_595/632565411.jpg</image:loc><image:caption>Now  you see it - the wall which was placed across the right of way at Longfellow Court, Mytholmroyd</image:caption><image:title>Now  you see it - the wall which was placed across the right of way at Longfellow Court, Mytholmroyd</image:title></image:image></url>
+</urlset>
\ No newline at end of file
diff --git a/src/test/resources/sitemaps/extension/shinpaideshou-news-sitemap.xml b/src/test/resources/sitemaps/extension/shinpaideshou-news-sitemap.xml
new file mode 100644
index 0000000..6253093
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/shinpaideshou-news-sitemap.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- generator="wordpress.com" -->
+<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"
+	xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+	xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"
+	xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
+	>
+<url><loc>https://shinpaideshou.wordpress.com/2017/04/25/job-opening-reischauer-policy-research-fellow/</loc><news:news><news:publication><news:name>What can I do with a B.A. in Japanese Studies?</news:name><news:language>en</news:language></news:publication><news:publication_date>2017-04-25T10:00:36+00:00</news:publication_date><news:title>Job Opening: Reischauer Policy Research Fellow</news:title><news:keywords>political science, policy, research fellow, Johns Hopkins</news:keywords></news:news></url><url><loc>https://shinpaideshou.wordpress.com/2017/04/24/call-for-papers-nation-gender-and-history-asian-cinemas-in-perspective-2/</loc><news:news><news:publication><news:name>What can I do with a B.A. in Japanese Studies?</news:name><news:language>en</news:language></news:publication><news:publication_date>2017-04-24T10:00:33+00:00</news:publication_date><news:title>Call for Papers: Nation, Gender and History, Asian Cinemas in Perspective</news:title><news:keywords>history, Asian Studies, film, gender, film studies, cinema, Vilnius University</news:keywords></news:news></url><url><loc>https://shinpaideshou.wordpress.com/2017/04/24/job-opening-lecturer-in-japanese-studies-university-of-leeds/</loc><news:news><news:publication><news:name>What can I do with a B.A. in Japanese Studies?</news:name><news:language>en</news:language></news:publication><news:publication_date>2017-04-24T10:00:10+00:00</news:publication_date><news:title>Job Opening: Lecturer in Japanese Studies, University of Leeds</news:title><news:keywords>japanese studies, job opening, lecturer, University of Leeds</news:keywords></news:news></url></urlset>
\ No newline at end of file
diff --git a/src/test/resources/sitemaps/extension/sitemap-images.xml b/src/test/resources/sitemaps/extension/sitemap-images.xml
new file mode 100644
index 0000000..85fc981
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/sitemap-images.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
+  <url>
+    <loc>http://www.example.com/images/some_image_landing_page.html</loc>
+    <image:image>
+      <image:loc>http://example.com/image.jpg</image:loc>
+    </image:image>
+    <image:image>
+      <image:loc>http://example.com/photo.jpg</image:loc>
+      <image:caption>This is the caption.</image:caption>
+      <image:geo_location>Limerick, Ireland</image:geo_location>
+      <image:title>Example photo shot in Limerick, Ireland</image:title>
+      <image:license>https://creativecommons.org/licenses/by/4.0/legalcode</image:license>
+    </image:image>
+  </url>
+</urlset>
diff --git a/src/test/resources/sitemaps/extension/sitemap-links.xml b/src/test/resources/sitemaps/extension/sitemap-links.xml
new file mode 100644
index 0000000..4ee04a2
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/sitemap-links.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
+  <url>
+    <loc>http://www.example.com/english/</loc>
+    <xhtml:link rel="alternate" hreflang="de" href="http://www.example.com/deutsch/" />
+    <xhtml:link rel="alternate" hreflang="de-ch" href="http://www.example.com/schweiz-deutsch/" />
+    <xhtml:link rel="alternate" hreflang="en" href="http://www.example.com/english/" />
+  </url>
+  <url>
+    <loc>http://www.example.com/deutsch/</loc>
+    <xhtml:link rel="alternate" hreflang="de" href="http://www.example.com/deutsch/" />
+    <xhtml:link rel="alternate" hreflang="de-ch" href="http://www.example.com/schweiz-deutsch/" />
+    <xhtml:link rel="alternate" hreflang="en" href="http://www.example.com/english/" />
+  </url>
+  <url>
+    <loc>http://www.example.com/schweiz-deutsch/</loc>
+    <xhtml:link rel="alternate" hreflang="de" href="http://www.example.com/deutsch/" />
+    <xhtml:link rel="alternate" hreflang="de-ch" href="http://www.example.com/schweiz-deutsch/" />
+    <xhtml:link rel="alternate" hreflang="en" href="http://www.example.com/english/" />
+  </url>
+</urlset>
diff --git a/src/test/resources/sitemaps/extension/sitemap-mobile.xml b/src/test/resources/sitemaps/extension/sitemap-mobile.xml
new file mode 100644
index 0000000..f1bc7dd
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/sitemap-mobile.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+        xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0">
+  <url>
+    <loc>http://www.example.com/mobile-friendly-1/</loc>
+    <mobile:mobile/>
+  </url>
+  <url>
+    <loc>http://www.example.com/mobile-friendly-2/</loc>
+    <mobile:mobile></mobile:mobile>
+  </url>
+  <url>
+    <loc>http://www.example.com/no-mobile/</loc>
+  </url>
+</urlset>
diff --git a/src/test/resources/sitemaps/extension/sitemap-news.xml b/src/test/resources/sitemaps/extension/sitemap-news.xml
new file mode 100644
index 0000000..d1f7b73
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/sitemap-news.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+        xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
+  <!-- example from https://support.google.com/news/publisher-center/answer/74288 -->
+  <url>
+    <loc>http://www.example.org/business/article55.html</loc>
+    <news:news>
+      <news:publication>
+        <news:name>The Example Times</news:name>
+        <news:language>en</news:language>
+      </news:publication>
+      <news:genres>PressRelease, Blog</news:genres>
+      <news:publication_date>2008-12-23</news:publication_date>
+      <news:title>Companies A, B in Merger Talks</news:title>
+      <news:keywords>business, merger, acquisition, A, B</news:keywords>
+      <news:stock_tickers>NASDAQ:A, NASDAQ:B</news:stock_tickers>
+    </news:news>
+  </url>
+</urlset>
diff --git a/src/test/resources/sitemaps/extension/sitemap-videos.xml b/src/test/resources/sitemaps/extension/sitemap-videos.xml
new file mode 100644
index 0000000..ad8415c
--- /dev/null
+++ b/src/test/resources/sitemaps/extension/sitemap-videos.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+        xmlns:video="http://www.google.com/schemas/sitemap-video/1.1" >
+  <!-- example from https://support.google.com/webmasters/answer/80471 -->
+  <url>
+    <loc>http://www.example.com/videos/some_video_landing_page.html</loc>
+    <video:video>
+      <video:thumbnail_loc>http://www.example.com/thumbs/123.jpg</video:thumbnail_loc>
+      <video:title>Grilling steaks for summer</video:title>
+      <video:description>Alkis shows you how to get perfectly done steaks every
+        time</video:description>
+      <video:content_loc>http://www.example.com/video123.flv</video:content_loc>
+      <video:player_loc allow_embed="yes" autoplay="ap=1">
+        http://www.example.com/videoplayer.swf?video=123</video:player_loc>
+      <video:duration>600</video:duration>
+      <video:expiration_date>2009-11-05T19:20:30+08:00</video:expiration_date>
+      <video:rating>4.2</video:rating>
+      <video:view_count>12345</video:view_count>
+      <video:publication_date>2007-11-05T19:20:30+08:00</video:publication_date>
+      <video:family_friendly>yes</video:family_friendly>
+      <video:tag>sample_tag1</video:tag>
+      <video:tag>sample_tag2</video:tag>
+      <video:restriction relationship="allow">IE GB US CA</video:restriction>
+      <video:gallery_loc title="Cooking Videos">http://cooking.example.com</video:gallery_loc>
+      <video:price currency="EUR">1.99</video:price>
+      <video:requires_subscription>yes</video:requires_subscription>
+      <video:uploader
+          info="http://www.example.com/users/grillymcgrillerson">GrillyMcGrillerson
+      </video:uploader>
+      <video:live>no</video:live>
+    </video:video>
+  </url>
+</urlset>