diff --git a/src/main/java/info/textgrid/services/aggregator/html/HTML.java b/src/main/java/info/textgrid/services/aggregator/html/HTML.java
index a7eaf595d87a8fe157f0af044dc2b3c22766c0e5..881204fb5b1058dae53ba140b000aa6a59030a7f 100644
--- a/src/main/java/info/textgrid/services/aggregator/html/HTML.java
+++ b/src/main/java/info/textgrid/services/aggregator/html/HTML.java
@@ -10,6 +10,7 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
 import info.textgrid.services.aggregator.GenericExceptionMapper;
 import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
 import info.textgrid.services.aggregator.TextGridRepProvider;
 import info.textgrid.services.aggregator.teicorpus.TEICorpusSerializer;
 
@@ -46,9 +47,10 @@
 
 import org.apache.cxf.jaxrs.model.wadl.Description;
 
+import com.google.common.base.Stopwatch;
+import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
 import com.google.common.cache.RemovalListener;
 import com.google.common.cache.RemovalNotification;
 import com.google.common.io.FileBackedOutputStream;
@@ -71,7 +73,7 @@ public class HTML {
 	@Context
 	private ServletContext servlet;
 
-	private LoadingCache<URI, XsltExecutable> stylesheets;
+	private Cache<URI, XsltExecutable> stylesheets;
 
 	private XsltExecutable getToHtml() {
 		if (toHtml == null) {
@@ -93,7 +95,12 @@ private XsltExecutable getToHtml() {
 	public HTML(final ITextGridRep repository) throws IOException {
 		this.repository = repository;
 		xsltProcessor = new Processor(false);
-		stylesheets = CacheBuilder.newBuilder().maximumSize(50).weakValues()
+		xsltProcessor.getUnderlyingConfiguration().setURIResolver(
+				new TGUriResolver(repository));
+		// xsltProcessor.getUnderlyingConfiguration().setAllowExternalFunctions(
+		// false); // we run external stylesheets
+		xsltProcessor.getUnderlyingConfiguration().setCompileWithTracing(true);
+		stylesheets = CacheBuilder.newBuilder().maximumSize(50).softValues()
 				.removalListener(new RemovalListener<URI, XsltExecutable>() {
 
 					@Override
@@ -127,6 +134,76 @@ public XsltExecutable load(final URI url) throws Exception {
 
 	}
 
+	/**
+	 * Returns an appropriate stylesheet for the given URI.
+	 * 
+	 * Basically, we try the following options in order:
+	 * <ol>
+	 * <li>The stylesheet is cached -> return the cached version.
+	 * <li>The stylesheet is public or external -> load & cache it.
+	 * <li>The stylesheet is non-public TextGrid internal -> load & do not cache
+	 * it.
+	 * </ol>
+	 * 
+	 * @param uri
+	 *            the URI of the stylesheet to load
+	 * @param sid
+	 *            the session ID to use, or null.
+	 * @param forceLoad
+	 *            TODO
+	 * @throws IOException
+	 *             if an error occurs reading the stylesheet.
+	 * @throws SaxonApiException
+	 *             if saxon fails to compile the stylesheet.
+	 */
+	protected XsltExecutable getStylesheet(final URI uri, final String sid,
+			final boolean forceLoad) throws SaxonApiException, IOException {
+		XsltExecutable executable = null;
+		
+		// (1) try cached version, if it exists
+		if (!forceLoad) {
+			executable = stylesheets.getIfPresent(uri);
+		}
+
+		if (executable == null) {
+
+			final XsltCompiler compiler = xsltProcessor.newXsltCompiler();
+			if (TGUriResolver.isResolveable(uri)) {
+
+				// (2/3) it's a TextGrid object, load it from TG-crud.
+				final TGOSupplier<InputStream> xsltSupplier = repository.read(uri, sid);
+				executable = compiler.compile(new StreamSource(xsltSupplier
+						.getInput(), uri.toString()));
+
+				if (isPublic(xsltSupplier.getMetadata())) {
+					// (2) it's public -> we can cache it.
+					stylesheets.put(uri, executable);
+					logger.log(Level.INFO, "Cached public stylesheet {0}", uri);
+				} else {
+					logger.log(Level.INFO, "Loaded private stylesheet {0}", uri);
+				}
+			} else {
+				// (2) it's non-TextGrid -- load & cache it.
+				executable = compiler.compile(new StreamSource(uri.toString()));
+				stylesheets.put(uri, executable);
+				logger.log(Level.INFO, "Cached external stylesheet {0}", uri);
+			}
+		} else {
+			logger.log(Level.INFO, "Reusing cached stylesheed {0}", uri);
+		}
+		
+		return executable;
+	}
+
+	private static boolean isPublic(final ObjectType metadata) {
+		try {
+			return metadata.getGeneric().getGenerated().getAvailability()
+					.contains("public");
+		} catch (final NullPointerException e) {
+			return false;
+		}
+	}
+
 	@GET
 	@Path(value = "/{object}")
 	@Produces(value = "text/html")
@@ -145,6 +222,9 @@ public StreamingOutput get(
 			SaxonApiException, ExecutionException {
 		logger.fine("HTML called for root object: " + uri);
 
+		final Stopwatch stopwatch = new Stopwatch();
+		stopwatch.start();
+
 		final TGCrudService crud = repository.getCRUDService();
 		final MetadataContainerType container = crud.readMetadata(sid, null,
 				uri.toString());
@@ -170,16 +250,18 @@ public StreamingOutput get(
 		} else {
 			tei = repository.getContent(uri, sid);
 		}
+		logger.info("we have an input document after " + stopwatch.toString());
 
 		final XsltTransformer transformer;
 		if (xsluri == null || "".equals(xsluri)) {
 			transformer = getToHtml().load();
 		} else {
-			if (refreshStylesheet) {
-				stylesheets.refresh(xsluri);
-			}
-			transformer = stylesheets.get(xsluri).load();
+			transformer = getStylesheet(xsluri, sid, refreshStylesheet).load();
+			if (sid != null) {
+				transformer.setURIResolver(new TGUriResolver(repository, sid));
+			} // otherwise default public URI resolver
 		}
+
 		transformer.setSource(new StreamSource(tei));
 		transformer.setParameter(new QName("graphicsURLPattern"),
 				new XdmAtomicValue(repository.getCRUDRestEndpoint()
@@ -190,6 +272,7 @@ public StreamingOutput get(
 			transformer.setParameter(new QName("cssFile"), new XdmAtomicValue(css));
 		}
 
+		logger.info("we're ready to transform after " + stopwatch.toString());
 		return new StreamingOutput() {
 
 			@Override
@@ -198,8 +281,9 @@ public void write(final OutputStream output) throws IOException,
 				transformer.setDestination(xsltProcessor.newSerializer(output));
 				try {
 					transformer.transform();
-					logger.info("Finished transformation to HTML for "
- + uri);
+					logger.info(MessageFormat
+							.format("Finished transformation to HTML for {0} after {1}",
+									uri, stopwatch.toString()));
 				} catch (final SaxonApiException e) {
 					throw new WebApplicationException(e);
 				}
diff --git a/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java b/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java
new file mode 100644
index 0000000000000000000000000000000000000000..13f3630f3ec6e0d8b3b24c9cdedce65a7dc121c9
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/html/TGUriResolver.java
@@ -0,0 +1,83 @@
+package info.textgrid.services.aggregator.html;
+
+import info.textgrid.services.aggregator.ITextGridRep;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.stream.StreamSource;
+
+import com.google.common.base.Optional;
+import com.ibm.icu.text.MessageFormat;
+
+public class TGUriResolver implements URIResolver {
+
+	private final Optional<String> sid;
+	private final String crudRestEndpoint;
+	final static Logger logger = Logger
+			.getLogger("info.textgrid.services.aggregator.html.URIResolver");
+
+	public TGUriResolver(final ITextGridRep repository,
+			final Optional<String> sid) {
+		super();
+		crudRestEndpoint = repository.getCRUDRestEndpoint();
+		this.sid = sid;
+	}
+
+	public TGUriResolver(final ITextGridRep repository) {
+		this(repository, Optional.<String> absent());
+	}
+
+	public TGUriResolver(final ITextGridRep repository, final String sid) {
+		this(repository, sid == null || "".equals(sid) ? Optional
+				.<String> absent() : Optional.of(sid));
+	}
+
+	public static boolean isResolveable(final URI uri) {
+		final String scheme = uri.getScheme();
+		return "textgrid".equals(scheme) || "hdl".equals(scheme);
+	}
+
+	@Override
+	public Source resolve(final String href, final String base) throws TransformerException {
+		logger.info(MessageFormat.format(
+				"Trying to resolve href={0}, base={1}", href, base));
+		try {
+			final URI uri = new URI(href).resolve(base);
+			if (isResolveable(uri)) {
+				final StringBuilder resolved = new StringBuilder(
+						crudRestEndpoint);
+				resolved.append('/').append(uri.getScheme()).append(':')
+						.append(uri.getSchemeSpecificPart()).append("/data");
+				if (sid.isPresent()) {
+					resolved.append("?sessionId=").append(sid.get());
+				}
+				if (uri.getFragment() != null) {
+					resolved.append('#').append(uri.getFragment());
+				}
+				final URL url = new URL(resolved.toString());
+				logger.log(Level.INFO,
+						MessageFormat.format("Resolved {0} to {1}", uri, url));
+				return new StreamSource(url.openStream(), href);
+			} else {
+				logger.log(Level.INFO, "Did not resolve {0}", uri);
+				return null;
+			}
+		} catch (final URISyntaxException e) {
+			throw new TransformerException(e);
+		} catch (final MalformedURLException e) {
+			throw new TransformerException(e);
+		} catch (final IOException e) {
+			throw new TransformerException(e);
+		}
+	}
+
+}