diff --git a/bt5/erp5_web/SkinTemplateItem/portal_skins/erp5_web/WebPage_exportAsSingleFile.py b/bt5/erp5_web/SkinTemplateItem/portal_skins/erp5_web/WebPage_exportAsSingleFile.py
index b0f971b50bd4f5accdf0a50a40da2bb83ffb9f0e..283b766f85f7eeb0f1b35cc447da0193a0b5d7f6 100644
--- a/bt5/erp5_web/SkinTemplateItem/portal_skins/erp5_web/WebPage_exportAsSingleFile.py
+++ b/bt5/erp5_web/SkinTemplateItem/portal_skins/erp5_web/WebPage_exportAsSingleFile.py
@@ -133,19 +133,19 @@ def handleHref(href):
   if not isHrefAUrl(href):
     return href
   try:
-    o = traverseHref(href)
+    obj = traverseHref(href)
   except (KeyError, Unauthorized):
     return makeHrefAbsolute(href)
-  return handleHrefObject(o, href)
+  return handleHrefObject(obj, href)
 
 def handleImageSource(src):
   if not isHrefAUrl(src):
     return src
   try:
-    o = traverseHref(src)
+    obj = traverseHref(src)
   except (KeyError, Unauthorized):
     return makeHrefAbsolute(src)
-  return handleImageSourceObject(o, src)
+  return handleImageSourceObject(obj, src)
 
 def replaceCssUrl(data):
   parts = context.Base_parseCssForUrl(data)
@@ -161,28 +161,28 @@ def replaceCssUrl(data):
       data += part[1]
   return data
 
-def handleImageSourceObject(o, src):
-  if hasattr(o, "convert"):
+def handleImageSourceObject(obj, src):
+  if hasattr(obj, "convert"):
     search = parseUrlSearch(extractUrlSearch(src))
     format_kw = {}
-    for k, x in search:
-      if k == "format" and x is not None:
-        format_kw["format"] = x
-      elif k == "display" and x is not None:
-        format_kw["display"] = x
+    for key, value in search:
+      if key == "format" and value is not None:
+        format_kw["format"] = value
+      elif key == "display" and value is not None:
+        format_kw["display"] = value
     if format_kw:
-      mime, data = o.convert(**format_kw)
+      mime, data = obj.convert(**format_kw)
       return handleLinkedData(mime, data, src)
 
-  return handleHrefObject(o, src, default_mimetype=bad_image_mime_type, default_data=bad_image_data)
+  return handleHrefObject(obj, src, default_mimetype=bad_image_mime_type, default_data=bad_image_data)
 
-def handleHrefObject(o, src, default_mimetype="text/html", default_data="<p>Linked page not found</p>"):
+def handleHrefObject(obj, src, default_mimetype="text/html", default_data="<p>Linked page not found</p>"):
   # handle File portal_skins/folder/file.png
   # XXX handle "?portal_skin=" parameter ?
-  if hasattr(o, "getContentType"):
-    mime = o.getContentType("")
+  if hasattr(obj, "getContentType"):
+    mime = obj.getContentType("")
     if mime:
-      data = getattr(o, "getData", lambda: str(o))() or ""
+      data = getattr(obj, "getData", lambda: str(obj))() or ""
       if isinstance(data, unicode):
         data = data.encode("utf-8")
       return handleLinkedData(mime, data, src)
@@ -191,8 +191,8 @@ def handleHrefObject(o, src, default_mimetype="text/html", default_data="<p>Link
   # handle Object.view
   # XXX handle url query parameters ? Not so easy because we need to
   # use the same behavior as when we call a script from browser URL bar.
-  if not hasattr(o, "getPortalType") and callable(o):
-    mime, data = "text/html", o()
+  if not hasattr(obj, "getPortalType") and callable(obj):
+    mime, data = "text/html", obj()
     if isinstance(data, unicode):
       data = data.encode("utf-8")
     return handleLinkedData(mime, data, src)
@@ -220,8 +220,12 @@ bad_image_mime_type = "image/png"
 
 request_protocol = context.REQUEST.SERVER_URL.split(":", 1)[0] + ":"
 site_object_dict = context.ERP5Site_getWebSiteDomainDict()
-base_url_root_object = portal
+base_url_root_object = getattr(context, "getWebSiteValue", str)() or portal
 base_url_object = context
+assert base_url_object.getRelativeUrl().startswith(base_url_root_object.getRelativeUrl())
+base_url = base_url_object.getRelativeUrl()[len(base_url_root_object.getRelativeUrl()):]
+if not base_url.startswith("/"):
+  base_url = "/" + base_url
 
 def handleLinkedData(mime, data, href):
   if format == "mhtml":
@@ -251,6 +255,7 @@ def isHrefAnAbsoluteUrl(href):
 def isHrefAUrl(href):
   return href.startswith("https://") or href.startswith("http://") or not href.split(":", 1)[0].isalpha()
 
+normalize_kw = {"keep_empty": False, "keep_trailing_slash": False}
 def traverseHref(url, allow_hash=False):
   url = url.split("?")[0]
   if not allow_hash:
@@ -258,16 +263,15 @@ def traverseHref(url, allow_hash=False):
   if url.startswith("https://") or url.startswith("http://") or url.startswith("//"):  # absolute url possibly on other sites
     site_url = "/".join(url.split("/", 3)[:3])
     domain = url.split("/", 3)[2]
+    site_object = site_object_dict[domain]
     relative_path = url[len(site_url):]
     relative_path = (relative_path[1:] if relative_path[:1] == "/" else relative_path)
-    site_object = site_object_dict.get(domain)
-    if site_object is None:
-      raise KeyError(relative_path.split("/")[0])
+    relative_path = context.Base_normalizeUrlPathname("/" + relative_path, **normalize_kw)[1:]
     return site_object.restrictedTraverse(str(relative_path))
   if url.startswith("/"):  # absolute path, relative url
-    return base_url_root_object.restrictedTraverse(str(url[1:]))
-  # relative url (just use a base url)
-  return base_url_object.restrictedTraverse(str(url))
+    return base_url_root_object.restrictedTraverse(str(context.Base_normalizeUrlPathname(url, **normalize_kw)[1:]))
+  # relative url
+  return base_url_root_object.restrictedTraverse(str(context.Base_normalizeUrlPathname(base_url + "/" + url, **normalize_kw)[1:]))
 
 def replaceFromDataUri(data_uri, replacer):
   header, data = data_uri.split(",")
@@ -290,16 +294,16 @@ def parseUrlSearch(search):
     search = search[1:]
   result = []
   for part in search.split("&"):
-    k = part.split("=")
-    v = "=".join(k[1:]) if len(k) else None
-    result.append((k[0], v))
+    key = part.split("=")
+    value = "=".join(key[1:]) if len(key) else None
+    result.append((key[0], value))
   return result
 
 def parseHtml(text):
   return context.Base_parseHtml(text)
 
-def escapeHtml(s):
-  return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
+def escapeHtml(text):
+  return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
 
 def anny(iterable, key=None):
   for i in iterable: