diff --git a/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java b/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java
index 0d6d858c7145dada26a6a3b9bd2586681d69d930..6ad973cd06068f6a51df0416820ec36e66713c8e 100644
--- a/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java
+++ b/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java
@@ -55,6 +55,7 @@ public class BlogInfo {
     public static final String SIGNATURE = "Signature";
     public static final String NAME = "Name";
     public static final String DESCRIPTION = "Description";
+    public static final String CONTACT_URL = "ContactURL";
     public static final String EDITION = "Edition";
     
     public void load(InputStream in) throws IOException {
diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java
index 3496690efed389f6c509e95865780b7516591222..847947b5e071b6fd8a0ab8967779124a331d3d93 100644
--- a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java
+++ b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java
@@ -400,8 +400,12 @@ public class HTMLRenderer extends EventReceiverImpl {
         if (location != null) {
             _bodyBuffer.append(" at ");
             SafeURL surl = new SafeURL(locationSchema + "://" + location);
-            _bodyBuffer.append("<a ").append(getClass("archiveSummaryLink")).append(" href=\"").append(getArchiveURL(null, surl));
-            _bodyBuffer.append("\">").append(sanitizeString(surl.toString())).append("</a>");
+            if (BlogManager.instance().authorizeRemote(_user)) {
+                _bodyBuffer.append("<a ").append(getClass("archiveSummaryLink")).append(" href=\"").append(getArchiveURL(null, surl));
+                _bodyBuffer.append("\">").append(sanitizeString(surl.toString())).append("</a>");
+            } else {
+                _bodyBuffer.append(sanitizeString(surl.getLocation()));
+            }
             if (_user.getAuthenticated()) {
                 _bodyBuffer.append(" <a ").append(getClass("archiveBookmarkLink")).append(" href=\"");
                 _bodyBuffer.append(getBookmarkURL(sanitizeString(name), surl.getLocation(), surl.getSchema(), "syndiearchive"));
@@ -1001,7 +1005,7 @@ public class HTMLRenderer extends EventReceiverImpl {
         if (_entry == null) return "unknown";
         return getMetadataURL(_entry.getURI().getKeyHash()); 
     }
-    public static String getMetadataURL(Hash blog) {
+    public String getMetadataURL(Hash blog) {
         return "viewmetadata.jsp?" + ArchiveViewerBean.PARAM_BLOG + "=" +
                Base64.encode(blog.getData());
     }
diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java
index a225008a3ffd460aa7d62cac177cc96861062d3d..77edf48f90126b2fa77e5a8ef9a3617561a65acb 100644
--- a/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java
+++ b/apps/syndie/java/src/net/i2p/syndie/sml/ThreadedHTMLRenderer.java
@@ -31,10 +31,19 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
     public static final String PARAM_VISIBLE = "visible";
     public static final String PARAM_ADD_TO_GROUP_LOCATION = "addLocation";
     public static final String PARAM_ADD_TO_GROUP_NAME = "addGroup";
+    /** name of the bookmarked entry to remove */
+    public static final String PARAM_REMOVE_FROM_GROUP_NAME = "removeName";
+    /** group to remove from the bookmarked entry, or if blank, remove the entry itself */
+    public static final String PARAM_REMOVE_FROM_GROUP = "removeGroup";
     /** index into the nav tree to start displaying */
     public static final String PARAM_OFFSET = "offset";
     public static final String PARAM_TAGS = "tags";
     public static final String PARAM_AUTHOR = "author";
+    // parameters for editing one's profile
+    public static final String PARAM_PROFILE_NAME = "profileName";
+    public static final String PARAM_PROFILE_DESC = "profileDesc";
+    public static final String PARAM_PROFILE_URL = "profileURL";
+    public static final String PARAM_PROFILE_OTHER = "profileOther";
     
     public static String getFilterByTagLink(String uri, ThreadNode node, User user, String tag, String author) { 
         StringBuffer buf = new StringBuffer(64);
@@ -399,6 +408,13 @@ public class ThreadedHTMLRenderer extends HTMLRenderer {
         //renderPreBodyCell();
     }
     
+    public String getMetadataURL(Hash blog) {
+        return buildProfileURL(blog);
+    }
+    public static String buildProfileURL(Hash blog) {
+        return "profile.jsp?" + ThreadedHTMLRenderer.PARAM_AUTHOR + "=" +
+               Base64.encode(blog.getData());
+    }
     protected String getEntryURL() { return getEntryURL(_user != null ? _user.getShowImages() : false); }
     protected String getEntryURL(boolean showImages) {
         if (_entry == null) 
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..29bde6acdbd4e880c2dccc570f85f5b3dcc1847e
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/web/BaseServlet.java
@@ -0,0 +1,716 @@
+package net.i2p.syndie.web;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.*;
+import net.i2p.data.*;
+import net.i2p.syndie.*;
+import net.i2p.syndie.data.*;
+import net.i2p.syndie.sml.*;
+
+/**
+ * Base servlet for handling request and rendering the templates
+ *
+ */
+public abstract class BaseServlet extends HttpServlet {
+    public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        req.setCharacterEncoding("UTF-8");
+        resp.setCharacterEncoding("UTF-8");
+        resp.setContentType("text/html");
+        
+        User user = (User)req.getSession().getAttribute("user");
+        String login = req.getParameter("login");
+        String pass = req.getParameter("password");
+        String action = req.getParameter("action");
+        boolean forceNewIndex = false;
+        
+        if (req.getParameter("regenerateIndex") != null)
+            forceNewIndex = true;
+        
+        if (user == null) {
+            if ("Login".equals(action)) {
+                user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized
+                if (!user.getAuthenticated())
+                    user = BlogManager.instance().getDefaultUser();
+            } else {
+                user = BlogManager.instance().getDefaultUser();
+            }
+            forceNewIndex = true;
+        } else if ("Login".equals(action)) {
+            user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized
+            if (!user.getAuthenticated())
+                user = BlogManager.instance().getDefaultUser();
+            forceNewIndex = true;
+        } else if ("Logout".equals(action)) {
+            user = BlogManager.instance().getDefaultUser();
+            forceNewIndex = true;
+        }
+        
+        req.getSession().setAttribute("user", user);
+        
+        forceNewIndex = handleBookmarking(user, req) || forceNewIndex;
+        handleUpdateProfile(user, req);
+        
+        FilteredThreadIndex index = (FilteredThreadIndex)req.getSession().getAttribute("threadIndex");
+        
+        Collection tags = getFilteredTags(req);
+        Collection filteredAuthors = getFilteredAuthors(req);
+        if (forceNewIndex || (index == null) || (!index.getFilteredTags().equals(tags)) || (!index.getFilteredAuthors().equals(filteredAuthors))) {
+            index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req), filteredAuthors);
+            req.getSession().setAttribute("threadIndex", index);
+        }
+        
+        render(user, req, resp.getWriter(), index);
+    }
+    
+    private boolean handleBookmarking(User user, HttpServletRequest req) {
+        if (!user.getAuthenticated())
+            return false;
+        
+        boolean rv = false;
+        
+        String loc = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION);
+        String group = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME);
+        if ( (loc != null) && (group != null) && (group.trim().length() > 0) ) {
+            try {
+                Hash key = new Hash();
+                key.fromBase64(loc);
+                PetNameDB db = user.getPetNameDB();
+                PetName pn = db.getByLocation(loc);
+                boolean isNew = false;
+                if (pn == null) {
+                    isNew = true;
+                    BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(key);
+                    String name = null;
+                    if (info != null)
+                        name = info.getProperty(BlogInfo.NAME);
+                    else
+                        name = loc.substring(0,6);
+
+                    if (db.containsName(name)) {
+                        int i = 0;
+                        while (db.containsName(name + i))
+                            i++;
+                        name = name + i;
+                    }
+
+                    pn = new PetName(name, "syndie", "syndieblog", loc);
+                }
+                pn.addGroup(group);
+                if (isNew)
+                    db.add(pn);
+                BlogManager.instance().saveUser(user);
+                // if we are ignoring someone, we need to recalculate the filters
+                if (FilteredThreadIndex.GROUP_IGNORE.equals(group))
+                    rv = true;
+            } catch (DataFormatException dfe) {
+                // bad loc, ignore
+            }
+        }
+        
+        String name = req.getParameter(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP_NAME);
+        group = req.getParameter(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP);
+        if ( (name != null) && (name.trim().length() > 0) ) {
+            PetNameDB db = user.getPetNameDB();
+            PetName pn = db.getByName(name);
+            boolean changed = false;
+            if (pn != null) {
+                if ( (group != null) && (group.trim().length() > 0) ) {
+                    // just remove them from the group
+                    changed = pn.isMember(group);
+                    pn.removeGroup(group);
+                    if ( (changed) && (FilteredThreadIndex.GROUP_IGNORE.equals(group)) )
+                        rv = true;
+                } else {
+                    // remove it completely
+                    if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE))
+                        rv = true;
+                    db.remove(pn);
+                    changed = true;
+                }
+            }
+            if (changed)
+                BlogManager.instance().saveUser(user);
+        }
+        
+        return rv;
+    }
+    
+    protected void handleUpdateProfile(User user, HttpServletRequest req) {
+        if ( (user == null) || (!user.getAuthenticated()) || (user.getBlog() == null) )
+            return;
+        
+        String action = req.getParameter("action");
+        if ( (action == null) || !("Update profile".equals(action)) )
+            return;
+        
+        String name = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_NAME);
+        String desc = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_DESC);
+        String url = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_URL);
+        String other = req.getParameter(ThreadedHTMLRenderer.PARAM_PROFILE_OTHER);
+        
+        Properties opts = new Properties();
+        if (!empty(name))
+            opts.setProperty(BlogInfo.NAME, name.trim());
+        if (!empty(desc))
+            opts.setProperty(BlogInfo.DESCRIPTION, desc.trim());
+        if (!empty(url))
+            opts.setProperty(BlogInfo.CONTACT_URL, url.trim());
+        if (!empty(other)) {
+            StringBuffer key = new StringBuffer();
+            StringBuffer val = null;
+            for (int i = 0; i < other.length(); i++) {
+                char c = other.charAt(i);
+                if ( (c == ':') || (c == '=') ) {
+                    if (val != null) {
+                        val.append(c);
+                    } else {
+                        val = new StringBuffer();
+                    }
+                } else if ( (c == '\n') || (c == '\r') ) {
+                    String k = key.toString().trim();
+                    String v = (val != null ? val.toString().trim() : "");
+                    if ( (k.length() > 0) && (v.length() > 0) ) {
+                        opts.setProperty(k, v);
+                    }
+                    key.setLength(0);
+                    val = null;
+                } else if (val != null) {
+                    val.append(c);
+                } else {
+                    key.append(c);
+                }
+            }
+            // now finish the last of it
+            String k = key.toString().trim();
+            String v = (val != null ? val.toString().trim() : "");
+            if ( (k.length() > 0) && (v.length() > 0) ) {
+                opts.setProperty(k, v);
+            }
+        }
+        
+        boolean updated = BlogManager.instance().updateMetadata(user, user.getBlog(), opts);
+    }
+    
+    protected void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException {
+        Archive archive = BlogManager.instance().getArchive();
+        int numThreads = 10;
+        int threadOffset = getOffset(req);
+        if (threadOffset == -1) {
+            threadOffset = index.getRootCount() - numThreads;
+        } 
+        if (threadOffset < 0) {
+            threadOffset = 0;
+        }
+
+        BlogURI visibleEntry = getVisible(req);
+        
+        int offset = 0;
+        if ( empty(req, ThreadedHTMLRenderer.PARAM_OFFSET) && (visibleEntry != null) ) {
+            // we're on a permalink, so jump the tree to the given thread
+            threadOffset = index.getRoot(visibleEntry);
+            if (threadOffset < 0)
+                threadOffset = 0;
+        }
+        
+        renderBegin(user, req, out, index);
+        renderNavBar(user, req, out, index);
+        renderControlBar(user, req, out, index);
+        renderServletDetails(user, req, out, index, threadOffset, visibleEntry, archive);
+        renderEnd(user, req, out, index);
+    }
+    
+    protected void renderBegin(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
+        out.write(BEGIN_HTML);
+    }
+    protected void renderNavBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
+        //out.write("<tr class=\"topNav\"><td class=\"topNav_user\" colspan=\"2\" nowrap=\"true\">\n");
+        out.write("<tr class=\"topNav\"><td colspan=\"3\" nowrap=\"true\"><span class=\"topNav_user\">\n");
+        out.write("<!-- nav bar begin -->\n");
+        if (user.getAuthenticated() && (user.getBlog() != null) ) {
+            out.write("Logged in as <a href=\"" + getProfileLink(req, user.getBlog()) + "\" title=\"Edit your profile\">");
+            out.write(user.getUsername());
+            out.write("</a>\n");
+            out.write("(<a href=\"switchuser.jsp\" title=\"Log in as another user\">switch</a>)\n");
+            out.write("<a href=\"post.jsp\" title=\"Post a new thread\">Post a new thread</a>\n");
+        } else {
+            out.write("<form action=\"" + req.getRequestURI() + "\" method=\"GET\">\n");
+            out.write("Login: <input type=\"text\" name=\"login\" />\n");
+            out.write("Password: <input type=\"password\" name=\"password\" />\n");
+            out.write("<input type=\"submit\" name=\"action\" value=\"Login\" /></form>\n");
+        }
+        //out.write("</td><td class=\"topNav_admin\">\n");
+        out.write("</span><span class=\"topNav_admin\">\n");
+        if (user.getAuthenticated() && user.getAllowAccessRemote()) {
+            out.write("<a href=\"syndicate.jsp\" title=\"Syndicate data between other Syndie nodes\">Syndicate</a>\n");
+            out.write("<a href=\"importfeed.jsp\" title=\"Import RSS/Atom data\">Import RSS/Atom</a>\n");
+            out.write("<a href=\"admin.jsp\" title=\"Configure this Syndie node\">Admin</a>\n");
+        }
+        out.write("</span><!-- nav bar end -->\n</td></tr>\n");
+    }
+    
+    protected static final ArrayList SKIP_TAGS = new ArrayList();
+    static {
+        SKIP_TAGS.add("action");
+        SKIP_TAGS.add("filter");
+        // post and visible are skipped since we aren't good at filtering by tag when the offset will
+        // skip around randomly.  at least, not yet.
+        SKIP_TAGS.add("visible");
+        //SKIP_TAGS.add("post");
+        //SKIP_TAGS.add("thread");
+        SKIP_TAGS.add("offset"); // if we are adjusting the filter, ignore the previous offset
+        SKIP_TAGS.add("login");
+        SKIP_TAGS.add("password");
+    }
+    
+    private static final String CONTROL_TARGET = "threads.jsp";
+    protected String getControlTarget() { return CONTROL_TARGET; }
+    
+    protected void renderControlBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
+        out.write("<form action=\"");
+        //out.write(req.getRequestURI());
+        out.write(getControlTarget());
+        out.write("\" method=\"GET\">\n");
+        String tags = "";
+        String author = "";
+        Enumeration params = req.getParameterNames();
+        while (params.hasMoreElements()) {
+            String param = (String)params.nextElement();
+            String val = req.getParameter(param);
+            if (ThreadedHTMLRenderer.PARAM_TAGS.equals(param)) {
+                tags = val;
+            } else if (ThreadedHTMLRenderer.PARAM_AUTHOR.equals(param)) {
+                author = val;
+            } else if (SKIP_TAGS.contains(param)) {
+                // skip
+            } else if (param.length() <= 0) {
+                // skip
+            } else {
+                out.write("<input type=\"hidden\" name=\"" + param + "\" value=\"" + val + "\" />\n");
+            }
+        }
+        out.write("<tr class=\"controlBar\"><td colspan=\"2\">\n");
+        out.write("<!-- control bar begin -->\n");
+        out.write("Filter: <select name=\"" + ThreadedHTMLRenderer.PARAM_AUTHOR + "\">\n");
+        
+        PetNameDB db = user.getPetNameDB();
+        TreeSet names = new TreeSet(db.getNames());
+        out.write("<option value=\"\">Any authors</option>\n");
+        if (user.getBlog() != null) {
+            if ( (author != null) && (author.equals(user.getBlog().toBase64())) )
+                out.write("<option value=\"" + user.getBlog().toBase64() + "\" selected=\"true\">Threads you posted in</option>\n");
+            else
+                out.write("<option value=\"" + user.getBlog().toBase64() + "\">Threads you posted in</option>\n");
+        }
+        
+        for (Iterator iter = names.iterator(); iter.hasNext(); ) {
+            String name = (String) iter.next();
+            PetName pn = db.getByName(name);
+            if ("syndieblog".equals(pn.getProtocol())) {
+                if ( (author != null) && (author.equals(pn.getLocation())) )
+                    out.write("<option value=\"" + pn.getLocation() + "\" selected=\"true\">Threads " + name + " posted in</option>\n");
+                else
+                    out.write("<option value=\"" + pn.getLocation() + "\">Threads " + name + " posted in</option>\n");
+            }
+        }
+        out.write("</select>\n");
+        
+        out.write("Tags: <input type=\"text\" name=\"" + ThreadedHTMLRenderer.PARAM_TAGS + "\" size=\"10\" value=\"" + tags + "\" />\n");
+
+        out.write("<input type=\"submit\" name=\"action\" value=\"Go\" />\n");
+        out.write("</td><td class=\"controlBarRight\"><a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a></td>\n");
+        out.write("<!-- control bar end -->\n");
+        out.write("</tr>\n");
+        out.write("</form>\n");
+    }
+   
+    protected abstract void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, 
+                                                 ThreadIndex index, int threadOffset, BlogURI visibleEntry, 
+                                                 Archive archive) throws IOException;
+    
+    protected static final int getOffset(HttpServletRequest req) {
+        String off = req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET);
+        try {
+            return Integer.parseInt(off);
+        } catch (NumberFormatException nfe) {
+            return 0;
+        }
+    }
+    protected static final BlogURI getVisible(HttpServletRequest req) {
+        return getAsBlogURI(req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE));
+    }
+    protected static final BlogURI getAsBlogURI(String uri) {
+        if (uri != null) {
+            int split = uri.indexOf('/');
+            if ( (split <= 0) || (split + 1 >= uri.length()) )
+                return null;
+            String blog = uri.substring(0, split);
+            String id = uri.substring(split+1);
+            try {
+                Hash hash = new Hash();
+                hash.fromBase64(blog);
+                long msgId = Long.parseLong(id);
+                if (msgId > 0)
+                    return new BlogURI(hash, msgId);
+            } catch (DataFormatException dfe) {
+                return null;
+            } catch (NumberFormatException nfe) {
+                return null;
+            }
+        }
+        return null;
+    }
+    
+
+    protected String trim(String orig, int maxLen) {
+        if ( (orig == null) || (orig.length() <= maxLen) )
+            return orig;
+        return orig.substring(0, maxLen) + "...";
+    }
+    
+    protected static final boolean empty(HttpServletRequest req, String param) {
+        String val = req.getParameter(param);
+        return (val == null) || (val.trim().length() <= 0);
+    }
+    
+    protected static final boolean empty(String val) {
+        return (val == null) || (val.trim().length() <= 0);
+    }
+    
+    protected String getExpandLink(HttpServletRequest req, ThreadNode node) {
+        return getExpandLink(node, req.getRequestURI(), req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
+                             req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), 
+                             req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
+                             req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
+                             req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
+    }
+    protected static String getExpandLink(ThreadNode node, String uri, String viewPost, String viewThread, 
+                                        String offset, String tags, String author) {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(uri);
+        buf.append('?');
+        // expand node == let one of node's children be visible
+        if (node.getChildCount() > 0) {
+            ThreadNode child = node.getChild(0);
+            buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
+            buf.append(child.getEntry().getKeyHash().toBase64()).append('/');
+            buf.append(child.getEntry().getEntryId()).append('&');
+        }
+        
+        if (!empty(viewPost))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
+        else if (!empty(viewThread))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
+        
+        if (!empty(offset))
+            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
+        
+        if (!empty(tags)) 
+            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
+        
+        if (!empty(author)) 
+            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
+        
+        return buf.toString();
+    }
+    protected String getCollapseLink(HttpServletRequest req, ThreadNode node) {
+        return getCollapseLink(node, req.getRequestURI(), 
+                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST),
+                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD),
+                               req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
+                               req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
+                               req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
+    }
+
+    protected String getCollapseLink(ThreadNode node, String uri, String viewPost, String viewThread, 
+                                   String offset, String tags, String author) { 
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(uri);
+        // collapse node == let the node be visible
+        buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
+        buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
+        buf.append(node.getEntry().getEntryId()).append('&');
+
+        if (!empty(viewPost))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
+        else if (!empty(viewThread))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
+        
+        if (!empty(offset))
+            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
+        
+        if (!empty(tags))
+            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
+        
+        if (!empty(author))
+            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
+        
+        return buf.toString();
+    }
+    protected String getProfileLink(HttpServletRequest req, Hash author) {
+        return getProfileLink(author);
+    }
+    protected String getProfileLink(Hash author) { return ThreadedHTMLRenderer.buildProfileURL(author); }
+    
+    protected String getAddToGroupLink(HttpServletRequest req, Hash author, User user, String group) {
+        return getAddToGroupLink(user, author, group, req.getRequestURI(), 
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE),
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD),
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), 
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
+    }
+    protected String getAddToGroupLink(User user, Hash author, String group, String uri, String visible,
+                                     String viewPost, String viewThread, String offset, String tags, String filteredAuthor) {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(uri);
+        buf.append('?');
+        if (!empty(visible))
+            buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(visible).append('&');
+        buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION).append('=').append(author.toBase64()).append('&');
+        buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME).append('=').append(group).append('&');
+
+        if (!empty(viewPost))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
+        else if (!empty(viewThread))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
+        
+        if (!empty(offset))
+            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
+
+        if (!empty(tags))
+            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
+        
+        if (!empty(filteredAuthor))
+            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(filteredAuthor).append('&');
+        
+        return buf.toString();
+    }
+    protected String getRemoveFromGroupLink(User user, String name, String group, String uri, String visible,
+                                            String viewPost, String viewThread, String offset, String tags, String filteredAuthor) {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(uri);
+        buf.append('?');
+        if (!empty(visible))
+            buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(visible).append('&');
+        buf.append(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP_NAME).append('=').append(name).append('&');
+        buf.append(ThreadedHTMLRenderer.PARAM_REMOVE_FROM_GROUP).append('=').append(group).append('&');
+
+        if (!empty(viewPost))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
+        else if (!empty(viewThread))
+            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
+        
+        if (!empty(offset))
+            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
+
+        if (!empty(tags))
+            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
+        
+        if (!empty(filteredAuthor))
+            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(filteredAuthor).append('&');
+        
+        return buf.toString();
+    }
+    protected String getViewPostLink(HttpServletRequest req, ThreadNode node, User user, boolean isPermalink) {
+        return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), node, user, isPermalink, 
+                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), 
+                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
+                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
+    }
+    protected String getViewThreadLink(HttpServletRequest req, ThreadNode node, User user) {
+        return getViewThreadLink(req.getRequestURI(), node, user,
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
+                                 req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
+    }
+    protected static String getViewThreadLink(String uri, ThreadNode node, User user, String offset,
+                                            String tags, String author) {
+        StringBuffer buf = new StringBuffer(64);
+        buf.append(uri);
+        if (node.getChildCount() > 0) {
+            buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
+            ThreadNode child = node.getChild(0);
+            buf.append(child.getEntry().getKeyHash().toBase64()).append('/');
+            buf.append(child.getEntry().getEntryId()).append('&');
+        } else {
+            buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
+            buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
+            buf.append(node.getEntry().getEntryId()).append('&');
+        }
+        buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=');
+        buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
+        buf.append(node.getEntry().getEntryId()).append('&');
+        
+        if (!empty(offset))
+            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
+        
+        if (!empty(tags))
+            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
+        
+        if (!empty(author))
+            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
+        
+        buf.append("#").append(node.getEntry().toString());
+        return buf.toString();
+    }
+    protected String getFilterByTagLink(HttpServletRequest req, ThreadNode node, User user, String tag, String author) { 
+        return ThreadedHTMLRenderer.getFilterByTagLink(req.getRequestURI(), node, user, tag, author);
+    }
+    protected String getNavLink(HttpServletRequest req, int offset) {
+        return ThreadedHTMLRenderer.getNavLink(req.getRequestURI(),
+                                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
+                                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), 
+                                               req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
+                                               req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), 
+                                               offset);
+    }
+    
+    protected void renderEnd(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
+        out.write(END_HTML);
+    }
+
+    protected Collection getFilteredTags(HttpServletRequest req) {
+        String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS);
+        if (tags != null) {
+            StringTokenizer tok = new StringTokenizer(tags, "\n\t ");
+            ArrayList rv = new ArrayList();
+            while (tok.hasMoreTokens()) {
+                String tag = tok.nextToken().trim();
+                if (tag.length() > 0)
+                    rv.add(tag);
+            }
+            return rv;
+        } else {
+            return Collections.EMPTY_LIST;
+        }
+    }
+    
+    protected Collection getFilteredAuthors(HttpServletRequest req) {
+        String authors = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
+        if (authors != null) {
+            StringTokenizer tok = new StringTokenizer(authors, "\n\t ");
+            ArrayList rv = new ArrayList();
+            while (tok.hasMoreTokens()) {
+                try {
+                    Hash h = new Hash();
+                    h.fromBase64(tok.nextToken().trim());
+                    rv.add(h);
+                } catch (DataFormatException dfe) {}
+            }
+            return rv;
+        } else {
+            return Collections.EMPTY_LIST;
+        }
+    }
+    
+    private static final String BEGIN_HTML = "<html>\n" +
+"<head>\n" +
+"<title>Syndie</title>\n" +
+"<style>\n" +
+".overallTable {\n" +
+"	border-spacing: 0px;\n" +
+"	border-width: 0px;\n" +
+"	border: 0px;\n" +
+"	margin: 0px;\n" +
+"	padding: 0px;\n" +
+"}\n" +
+".topNav {\n" +
+"	background-color: #BBBBBB;\n" +
+"}\n" +
+".topNav_user {\n" +
+"	text-align: left;\n" +
+"	float: left;\n" +
+"	align: left;\n" +
+"	display: inline;\n" +
+"}\n" +
+".topNav_admin {\n" +
+"	text-align: right;\n" +
+"	float: right;\n" +
+"	align: right;\n" +
+"	display: inline;\n" +
+"}\n" +
+".controlBar {\n" +
+"	background-color: #BBBBBB;\n" +
+"}\n" +
+".controlBarRight {\n" +
+"	text-align: right;\n" +
+"}\n" +
+".threadEven {\n" +
+"	background-color: #FFFFFF;\n" +
+"	white-space: nowrap;\n" +
+"}\n" +
+".threadOdd {\n" +
+"	background-color: #EEEEEE;\n" +
+"	white-space: nowrap;\n" +
+"}\n" +
+".threadLeft {\n" +
+"	text-align: left;\n" +
+"	align: left;\n" +
+"}\n" +
+".threadRight {\n" +
+"	text-align: right;\n" +
+"}\n" +
+".threadNav {\n" +
+"	background-color: #BBBBBB;\n" +
+"}\n" +
+".threadNavRight {\n" +
+"	text-align: right;\n" +
+"}\n" +
+".postMeta {\n" +
+"	background-color: #BBBBFF;\n" +
+"}\n" +
+".postMetaSubject {\n" +
+"	text-align: left;\n" +
+"}\n" +
+".postMetaLink {\n" +
+"	text-align: right;\n" +
+"}\n" +
+".postDetails {\n" +
+"	background-color: #DDDDFF;\n" +
+"}\n" +
+".postReply {\n" +
+"	background-color: #BBBBFF;\n" +
+"}\n" +
+".postReplyText {\n" +
+"	background-color: #BBBBFF;\n" +
+"}\n" +
+".postReplyOptions {\n" +
+"	background-color: #BBBBFF;\n" +
+"}\n" +
+"</style>\n" +
+"<link href=\"style.jsp\" rel=\"stylesheet\" type=\"text/css\" >\n" +
+"<link href=\"rss.jsp\" rel=\"alternate\" type=\"application/rss+xml\" >\n" +
+"</head>\n" +
+"<body>\n" +
+"<span style=\"display: none\"><a href=\"#bodySubject\">Jump to the beginning of the first post rendered, if any</a>\n" +
+"<a href=\"#threads\">Jump to the thread navigation</a>\n</span>\n" +
+"<table border=\"0\" width=\"100%\" class=\"overallTable\">\n";
+    
+    private static final String END_HTML = "</table>\n" +
+"</body>\n";
+   
+    protected static class TreeRenderState {
+        private int _rowsWritten;
+        private int _rowsSkipped;
+        private List _ignored;
+        public TreeRenderState(List ignored) { 
+            _rowsWritten = 0; 
+            _rowsSkipped = 0;
+            _ignored = ignored;
+        }
+        public int getRowsWritten() { return _rowsWritten; }
+        public void incrementRowsWritten() { _rowsWritten++; }
+        public int getRowsSkipped() { return _rowsSkipped; }
+        public void incrementRowsSkipped() { _rowsSkipped++; }
+        public List getIgnoredAuthors() { return _ignored; }
+    }
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ddbfc9b3987b53356f100347e4b12cd1c9cfbc3
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java
@@ -0,0 +1,209 @@
+package net.i2p.syndie.web;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.*;
+import net.i2p.data.*;
+import net.i2p.syndie.*;
+import net.i2p.syndie.data.*;
+import net.i2p.syndie.sml.*;
+
+/**
+ * Render the requested profile
+ *
+ */
+public class ProfileServlet extends BaseServlet {
+    protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, 
+                                        int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
+        Hash author = null;
+        String str = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
+        if (str != null) {
+            try {
+                author = new Hash();
+                author.fromBase64(str);
+            } catch (DataFormatException dfe) {
+                author = null;
+            }
+        } else {
+            author = user.getBlog();
+        }
+        
+        String uri = req.getRequestURI();
+        
+        if (author == null) {
+            renderInvalidProfile(out);
+        } else if ( (user.getBlog() != null) && (user.getBlog().equals(author)) ) {
+            renderMyProfile(user, uri, out, archive);
+        } else {
+            renderProfile(user, uri, out, author, archive);
+        }
+    }   
+    
+    private void renderInvalidProfile(PrintWriter out) throws IOException {
+        out.write(INVALID_PROFILE);
+    }
+    
+    private void renderMyProfile(User user, String baseURI, PrintWriter out, Archive archive) throws IOException {
+        BlogInfo info = archive.getBlogInfo(user.getBlog());
+        if (info == null)
+            return;
+        
+        out.write("<!-- " + info.toString() + "-->\n");
+        out.write("<form action=\"" + baseURI + "\" method=\"POST\">\n");
+        // now add the form to update
+        out.write("<tr><td colspan=\"3\">Your profile</td></tr>\n");
+        out.write("<tr><td colspan=\"3\">Name: <input type=\"text\" name=\"" 
+                  + ThreadedHTMLRenderer.PARAM_PROFILE_NAME + "\" value=\"" 
+                  + HTMLRenderer.sanitizeTagParam(info.getProperty(BlogInfo.NAME)) + "\"></td></tr>\n");
+        out.write("<tr><td colspan=\"3\">Account description: <input type=\"text\" name=\"" 
+                  + ThreadedHTMLRenderer.PARAM_PROFILE_DESC + "\" value=\"" 
+                  + HTMLRenderer.sanitizeTagParam(info.getProperty(BlogInfo.DESCRIPTION)) + "\"></td></tr>\n");
+        out.write("<tr><td colspan=\"3\">Contact information: <input type=\"text\" name=\"" 
+                  + ThreadedHTMLRenderer.PARAM_PROFILE_URL + "\" value=\"" 
+                  + HTMLRenderer.sanitizeTagParam(info.getProperty(BlogInfo.CONTACT_URL)) + "\"></td></tr>\n");
+        out.write("<tr><td colspan=\"3\">Other attributes:<br /><textarea rows=\"3\" name=\"" 
+                  + ThreadedHTMLRenderer.PARAM_PROFILE_OTHER + "\" cols=\"60\">");
+        String props[] = info.getProperties();
+        if (props != null) {
+            for (int i = 0; i < props.length; i++) {
+                if (!BlogInfo.NAME.equals(props[i]) && 
+                    !BlogInfo.DESCRIPTION.equals(props[i]) && 
+                    !BlogInfo.EDITION.equals(props[i]) && 
+                    !BlogInfo.OWNER_KEY.equals(props[i]) && 
+                    !BlogInfo.POSTERS.equals(props[i]) && 
+                    !BlogInfo.SIGNATURE.equals(props[i]) &&
+                    !BlogInfo.CONTACT_URL.equals(props[i])) {
+                    out.write(HTMLRenderer.sanitizeString(props[i], false) + ": " 
+                              + HTMLRenderer.sanitizeString(info.getProperty(props[i]), false) + "\n");
+                }
+            }
+        }
+        out.write("</textarea></td></tr>\n");
+
+        out.write("<tr><td colspan=\"3\"><input type=\"submit\" name=\"action\" value=\"Update profile\" /></td></tr>\n");
+        out.write("</form>\n");
+    }
+    
+    private void renderProfile(User user, String baseURI, PrintWriter out, Hash author, Archive archive) throws IOException {
+        out.write("<tr><td colspan=\"3\">Profile for <a href=\"" + getControlTarget() + "?" 
+                  + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + author.toBase64() 
+                  + "\" title=\"View threads by the profiled author\">");
+        PetName pn = user.getPetNameDB().getByLocation(author.toBase64());
+        BlogInfo info = archive.getBlogInfo(author);
+        if (pn != null) {
+            out.write(pn.getName());
+            String name = null;
+            if (info != null)
+                name = info.getProperty(BlogInfo.NAME);
+            
+            if ( (name == null) || (name.trim().length() <= 0) )
+                name = author.toBase64().substring(0, 6);
+            
+            out.write(" (" + name + ")");
+        } else {
+            String name = null;
+            if (info != null)
+                name = info.getProperty(BlogInfo.NAME);
+            
+            if ( (name == null) || (name.trim().length() <= 0) )
+                name = author.toBase64().substring(0, 6);
+            out.write(name);
+        }
+        out.write("</a>");
+        if (info != null)
+            out.write(" [edition " + info.getEdition() + "]");
+        out.write("</td></tr>\n");
+        
+        out.write("<tr><td colspan=\"3\"><hr /></td></tr>\n");
+        if (pn == null) {
+            out.write("<tr><td colspan=\"3\">Not currently bookmarked.  Add them to your ");
+            String addFav = getAddToGroupLink(user, author, FilteredThreadIndex.GROUP_FAVORITE, 
+                                              baseURI, "", "", "", "", "", author.toBase64());
+            String addIgnore = getAddToGroupLink(user, author, FilteredThreadIndex.GROUP_IGNORE, 
+                                                 baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<a href=\"" + addFav + "\" title=\"Threads by favorite authors are shown specially\">favorites</a> or ");
+            out.write("<a href=\"" + addIgnore + "\" title=\"Threads by ignored authors are hidden from view\">ignored</a> ");
+            out.write("</td></tr>\n");
+        } else if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE)) {
+            out.write("<tr><td colspan=\"3\">Currently ignored - threads they create are hidden.</td></tr>\n");
+            String remIgnore = getRemoveFromGroupLink(user, pn.getName(), FilteredThreadIndex.GROUP_IGNORE, 
+                                                      baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + remIgnore + "\">Unignore " + pn.getName() + "</a></td></tr>\n");
+            String remCompletely = getRemoveFromGroupLink(user, pn.getName(), "", 
+                                                          baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + remCompletely + "\">Forget about " + pn.getName() + " entirely</a></td></tr>\n");
+        } else if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) {
+            out.write("<tr><td colspan=\"3\">Currently marked as a favorite author - threads they participate in " +
+                       "are highlighted.</td></tr>\n");
+            String remIgnore = getRemoveFromGroupLink(user, pn.getName(), FilteredThreadIndex.GROUP_FAVORITE, 
+                                                      baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + remIgnore + "\">Remove " + pn.getName() + " from the list of favorite authors</a></td></tr>\n");
+            String addIgnore = getAddToGroupLink(user, author, FilteredThreadIndex.GROUP_IGNORE, 
+                                                 baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + addIgnore + "\" title=\"Threads by ignored authors are hidden from view\">Ignore the author</a></td></tr>");
+            String remCompletely = getRemoveFromGroupLink(user, pn.getName(), "", 
+                                                          baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + remCompletely + "\">Forget about " + pn.getName() + " entirely</a></td></tr>\n");
+        } else {
+            out.write("<tr><td colspan=\"3\">Currently bookmarked.  Add them to your ");
+            String addFav = getAddToGroupLink(user, author, FilteredThreadIndex.GROUP_FAVORITE, 
+                                              baseURI, "", "", "", "", "", author.toBase64());
+            String addIgnore = getAddToGroupLink(user, author, FilteredThreadIndex.GROUP_IGNORE, 
+                                                 baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<a href=\"" + addFav + "\" title=\"Threads by favorite authors are shown specially\">favorites</a> or ");
+            out.write("<a href=\"" + addIgnore + "\" title=\"Threads by ignored authors are hidden from view\">ignored</a> list</td></tr>");
+            String remCompletely = getRemoveFromGroupLink(user, pn.getName(), "", 
+                                                          baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td></td><td colspan=\"2\"><a href=\"" + remCompletely + "\">Forget about " + pn.getName() + " entirely</a></td></tr>\n");
+        }
+        
+        if (info != null) {
+            String descr = info.getProperty(BlogInfo.DESCRIPTION);
+            if ( (descr != null) && (descr.trim().length() > 0) )
+                out.write("<tr><td colspan=\"3\">Account description: " + HTMLRenderer.sanitizeString(descr) + "</td></tr>\n");
+            
+            String contactURL = info.getProperty(BlogInfo.CONTACT_URL);
+            if ( (contactURL != null) && (contactURL.trim().length() > 0) )
+                out.write("<tr><td colspan=\"3\">Contact information: "
+                          + HTMLRenderer.sanitizeString(contactURL) + "</td></tr>\n");
+            
+            String props[] = info.getProperties();
+            int altCount = 0;
+            if (props != null)
+                for (int i = 0; i < props.length; i++)
+                    if (!BlogInfo.NAME.equals(props[i]) && 
+                        !BlogInfo.DESCRIPTION.equals(props[i]) && 
+                        !BlogInfo.EDITION.equals(props[i]) && 
+                        !BlogInfo.OWNER_KEY.equals(props[i]) && 
+                        !BlogInfo.POSTERS.equals(props[i]) && 
+                        !BlogInfo.SIGNATURE.equals(props[i]) &&
+                        !BlogInfo.CONTACT_URL.equals(props[i]))
+                        altCount++;
+            if (altCount > 0) {
+                for (int i = 0; i < props.length; i++) {
+                    if (!BlogInfo.NAME.equals(props[i]) && 
+                        !BlogInfo.DESCRIPTION.equals(props[i]) && 
+                        !BlogInfo.EDITION.equals(props[i]) && 
+                        !BlogInfo.OWNER_KEY.equals(props[i]) && 
+                        !BlogInfo.POSTERS.equals(props[i]) && 
+                        !BlogInfo.SIGNATURE.equals(props[i]) &&
+                        !BlogInfo.CONTACT_URL.equals(props[i])) {
+                        out.write("<tr><td colspan=\"3\">");
+                        out.write(HTMLRenderer.sanitizeString(props[i]) + ": " 
+                                  + HTMLRenderer.sanitizeString(info.getProperty(props[i])));
+                        out.write("</td></tr>\n");
+                    }
+                }
+            }
+        }
+    }
+    
+    private static final String INVALID_PROFILE = "<tr><td colspan=\"3\">The profile requested is invalid</td></tr>\n";
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java
index 45bd08c5f00d22df3792c8a757797133e44016bf..da805d42f38ea31f4b08c9f422544b5c34fc5af8 100644
--- a/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ViewThreadedServlet.java
@@ -6,7 +6,6 @@ import java.util.*;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.ServletException;
 
 import net.i2p.I2PAppContext;
 import net.i2p.client.naming.*;
@@ -16,225 +15,20 @@ import net.i2p.syndie.data.*;
 import net.i2p.syndie.sml.*;
 
 /**
+ * Render the appropriate posts and the thread tree
  *
  */
-public class ViewThreadedServlet extends HttpServlet {
-    public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
-        req.setCharacterEncoding("UTF-8");
-        resp.setCharacterEncoding("UTF-8");
-        resp.setContentType("text/html");
-        
-        User user = (User)req.getSession().getAttribute("user");
-        String login = req.getParameter("login");
-        String pass = req.getParameter("password");
-        String action = req.getParameter("action");
-        boolean forceNewIndex = false;
-        
-        if (req.getParameter("regenerateIndex") != null)
-            forceNewIndex = true;
-        
-        if (user == null) {
-            if ("Login".equals(action)) {
-                user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized
-                if (!user.getAuthenticated())
-                    user.invalidate();
-            } else {
-                user = new User();
-            }
-            forceNewIndex = true;
-        } else if ("Login".equals(action)) {
-            user = BlogManager.instance().login(login, pass); // ignore failures - user will just be unauthorized
-            forceNewIndex = true;
-        } else if ("Logout".equals(action)) {
-            user = new User();
-            forceNewIndex = true;
-        }
-        
-        req.getSession().setAttribute("user", user);
-        
-        if (user.getAuthenticated()) {
-            String loc = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION);
-            String group = req.getParameter(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME);
-            if ( (loc != null) && (group != null) && (group.trim().length() > 0) ) {
-                try {
-                    Hash key = new Hash();
-                    key.fromBase64(loc);
-                    PetNameDB db = user.getPetNameDB();
-                    PetName pn = db.getByLocation(loc);
-                    boolean isNew = false;
-                    if (pn == null) {
-                        isNew = true;
-                        BlogInfo info = BlogManager.instance().getArchive().getBlogInfo(key);
-                        String name = null;
-                        if (info != null)
-                            name = info.getProperty(BlogInfo.NAME);
-                        else
-                            name = loc.substring(0,6);
-
-                        if (db.containsName(name)) {
-                            int i = 0;
-                            while (db.containsName(name + i))
-                                i++;
-                            name = name + i;
-                        }
-                        
-                        pn = new PetName(name, "syndie", "syndieblog", loc);
-                    }
-                    pn.addGroup(group);
-                    if (isNew)
-                        db.add(pn);
-                    BlogManager.instance().saveUser(user);
-                    // if we are ignoring someone, we need to recalculate the filters
-                    if (FilteredThreadIndex.GROUP_IGNORE.equals(group))
-                        forceNewIndex = true;
-                } catch (DataFormatException dfe) {
-                    // bad loc, ignore
-                }
-            }
-        }
-        
-        FilteredThreadIndex index = (FilteredThreadIndex)req.getSession().getAttribute("threadIndex");
-        
-        Collection tags = getFilteredTags(req);
-        Collection filteredAuthors = getFilteredAuthors(req);
-        if (forceNewIndex || (index == null) || (!index.getFilteredTags().equals(tags)) || (!index.getFilteredAuthors().equals(filteredAuthors))) {
-            index = new FilteredThreadIndex(user, BlogManager.instance().getArchive(), getFilteredTags(req), filteredAuthors);
-            req.getSession().setAttribute("threadIndex", index);
-        }
-        
-        render(user, req, resp.getWriter(), index);
-    }
-    
-    private void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException {
-        Archive archive = BlogManager.instance().getArchive();
-        int numThreads = 10;
-        int threadOffset = getOffset(req);
-        if (threadOffset == -1) {
-            threadOffset = index.getRootCount() - numThreads;
-        } 
-        if (threadOffset < 0) {
-            threadOffset = 0;
-        }
-
-        BlogURI visibleEntry = getVisible(req);
-        
-        int offset = 0;
-        if ( empty(req, ThreadedHTMLRenderer.PARAM_OFFSET) && (visibleEntry != null) ) {
-            // we're on a permalink, so jump the tree to the given thread
-            threadOffset = index.getRoot(visibleEntry);
-            if (threadOffset < 0)
-                threadOffset = 0;
-        }
-        
-        renderBegin(user, req, out, index);
-        renderNavBar(user, req, out, index);
-        renderControlBar(user, req, out, index);
+public class ViewThreadedServlet extends BaseServlet {
+    protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, 
+                                        int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
         renderBody(user, req, out, index);
         
         renderThreadNav(user, req, out, threadOffset, index);
         renderThreadTree(user, req, out, threadOffset, visibleEntry, archive, index);
         renderThreadNav(user, req, out, threadOffset, index);
-        renderEnd(user, req, out, index);
-    }
+    }   
     
-    private void renderBegin(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
-        out.write(BEGIN_HTML);
-    }
-    private void renderNavBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
-        //out.write("<tr class=\"topNav\"><td class=\"topNav_user\" colspan=\"2\" nowrap=\"true\">\n");
-        out.write("<tr class=\"topNav\"><td colspan=\"3\" nowrap=\"true\"><span class=\"topNav_user\">\n");
-        out.write("<!-- nav bar begin -->\n");
-        if (user.getAuthenticated()) {
-            out.write("Logged in as <a href=\"" + getProfileLink(req, user.getBlog()) + "\" title=\"Edit your profile\">");
-            out.write(user.getUsername());
-            out.write("</a>\n");
-            out.write("(<a href=\"switchuser.jsp\" title=\"Log in as another user\">switch</a>)\n");
-            out.write("<a href=\"post.jsp\" title=\"Post a new thread\">Post a new thread</a>\n");
-        } else {
-            out.write("<form action=\"" + req.getRequestURI() + "\" method=\"GET\">\n");
-            out.write("Login: <input type=\"text\" name=\"login\" />\n");
-            out.write("Password: <input type=\"password\" name=\"password\" />\n");
-            out.write("<input type=\"submit\" name=\"action\" value=\"Login\" /></form>\n");
-        }
-        //out.write("</td><td class=\"topNav_admin\">\n");
-        out.write("</span><span class=\"topNav_admin\">\n");
-        if (user.getAuthenticated() && user.getAllowAccessRemote()) {
-            out.write("<a href=\"syndicate.jsp\" title=\"Syndicate data between other Syndie nodes\">Syndicate</a>\n");
-            out.write("<a href=\"importfeed.jsp\" title=\"Import RSS/Atom data\">Import RSS/Atom</a>\n");
-            out.write("<a href=\"admin.jsp\" title=\"Configure this Syndie node\">Admin</a>\n");
-        }
-        out.write("</span><!-- nav bar end -->\n</td></tr>\n");
-    }
-    
-    private static final ArrayList SKIP_TAGS = new ArrayList();
-    static {
-        SKIP_TAGS.add("action");
-        SKIP_TAGS.add("filter");
-        // post and visible are skipped since we aren't good at filtering by tag when the offset will
-        // skip around randomly.  at least, not yet.
-        SKIP_TAGS.add("visible");
-        //SKIP_TAGS.add("post");
-        //SKIP_TAGS.add("thread");
-        SKIP_TAGS.add("offset"); // if we are adjusting the filter, ignore the previous offset
-        SKIP_TAGS.add("login");
-        SKIP_TAGS.add("password");
-    }
-    
-    private void renderControlBar(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
-        out.write("<form action=\"");
-        out.write(req.getRequestURI());
-        out.write("\" method=\"GET\">\n");
-        String tags = "";
-        String author = "";
-        Enumeration params = req.getParameterNames();
-        while (params.hasMoreElements()) {
-            String param = (String)params.nextElement();
-            String val = req.getParameter(param);
-            if (ThreadedHTMLRenderer.PARAM_TAGS.equals(param)) {
-                tags = val;
-            } else if (ThreadedHTMLRenderer.PARAM_AUTHOR.equals(param)) {
-                author = val;
-            } else if (SKIP_TAGS.contains(param)) {
-                // skip
-            } else if (param.length() <= 0) {
-                // skip
-            } else {
-                out.write("<input type=\"hidden\" name=\"" + param + "\" value=\"" + val + "\" />\n");
-            }
-        }
-        out.write("<tr class=\"controlBar\"><td colspan=\"2\">\n");
-        out.write("<!-- control bar begin -->\n");
-        out.write("Filter: <select name=\"" + ThreadedHTMLRenderer.PARAM_AUTHOR + "\">\n");
-        
-        PetNameDB db = user.getPetNameDB();
-        TreeSet names = new TreeSet(db.getNames());
-        out.write("<option value=\"\">Any authors</option>\n");
-        if (author.equals(user.getBlog().toBase64()))
-            out.write("<option value=\"" + user.getBlog().toBase64() + "\" selected=\"true\">Threads you posted in</option>\n");
-        else
-            out.write("<option value=\"" + user.getBlog().toBase64() + "\">Threads you posted in</option>\n");
-        
-        for (Iterator iter = names.iterator(); iter.hasNext(); ) {
-            String name = (String) iter.next();
-            PetName pn = db.getByName(name);
-            if ("syndieblog".equals(pn.getProtocol())) {
-                if (author.equals(pn.getLocation()))
-                    out.write("<option value=\"" + pn.getLocation() + "\" selected=\"true\">Threads " + name + " posted in</option>\n");
-                else
-                    out.write("<option value=\"" + pn.getLocation() + "\">Threads " + name + " posted in</option>\n");
-            }
-        }
-        out.write("</select>\n");
-        
-        out.write("Tags: <input type=\"text\" name=\"" + ThreadedHTMLRenderer.PARAM_TAGS + "\" size=\"10\" value=\"" + tags + "\" />\n");
-
-        out.write("<input type=\"submit\" name=\"action\" value=\"Go\" />\n");
-        out.write("</td><td class=\"controlBarRight\"><a href=\"#threads\" title=\"Jump to the thread navigation\">Threads</a></td>\n");
-        out.write("<!-- control bar end -->\n");
-        out.write("</tr>\n");
-        out.write("</form>\n");
-    }
-    private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException, ServletException  {
+    private void renderBody(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException  {
         ThreadedHTMLRenderer renderer = new ThreadedHTMLRenderer(I2PAppContext.getGlobalContext());
         Archive archive = BlogManager.instance().getArchive();
         List posts = getPosts(archive, req, index);
@@ -323,74 +117,7 @@ public class ViewThreadedServlet extends HttpServlet {
         int numThreads = 10;
         renderThreadTree(user, out, index, archive, req, threadOffset, numThreads, visibleEntry);
     }
-    
-    private static final int getOffset(HttpServletRequest req) {
-        String off = req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET);
-        try {
-            return Integer.parseInt(off);
-        } catch (NumberFormatException nfe) {
-            return 0;
-        }
-    }
-    private static final BlogURI getVisible(HttpServletRequest req) {
-        return getAsBlogURI(req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE));
-    }
-    private static final BlogURI getAsBlogURI(String uri) {
-        if (uri != null) {
-            int split = uri.indexOf('/');
-            if ( (split <= 0) || (split + 1 >= uri.length()) )
-                return null;
-            String blog = uri.substring(0, split);
-            String id = uri.substring(split+1);
-            try {
-                Hash hash = new Hash();
-                hash.fromBase64(blog);
-                long msgId = Long.parseLong(id);
-                if (msgId > 0)
-                    return new BlogURI(hash, msgId);
-            } catch (DataFormatException dfe) {
-                return null;
-            } catch (NumberFormatException nfe) {
-                return null;
-            }
-        }
-        return null;
-    }
-    
-    private Collection getFilteredTags(HttpServletRequest req) {
-        String tags = req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS);
-        if (tags != null) {
-            StringTokenizer tok = new StringTokenizer(tags, "\n\t ");
-            ArrayList rv = new ArrayList();
-            while (tok.hasMoreTokens()) {
-                String tag = tok.nextToken().trim();
-                if (tag.length() > 0)
-                    rv.add(tag);
-            }
-            return rv;
-        } else {
-            return Collections.EMPTY_LIST;
-        }
-    }
-    
-    private Collection getFilteredAuthors(HttpServletRequest req) {
-        String authors = req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR);
-        if (authors != null) {
-            StringTokenizer tok = new StringTokenizer(authors, "\n\t ");
-            ArrayList rv = new ArrayList();
-            while (tok.hasMoreTokens()) {
-                try {
-                    Hash h = new Hash();
-                    h.fromBase64(tok.nextToken().trim());
-                    rv.add(h);
-                } catch (DataFormatException dfe) {}
-            }
-            return rv;
-        } else {
-            return Collections.EMPTY_LIST;
-        }
-    }
-
+     
     private void renderThreadTree(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req,
                                   int threadOffset, int numThreads, BlogURI visibleEntry) {
         
@@ -421,12 +148,10 @@ public class ViewThreadedServlet extends HttpServlet {
         out.write("<!-- threads end -->\n");
     }
     
-    /**
-     * @return true if some post in the thread has been written
-     */
     private boolean renderThread(User user, PrintWriter out, ThreadIndex index, Archive archive, HttpServletRequest req,
                                  ThreadNode node, int depth, BlogURI visibleEntry, TreeRenderState state) {
         boolean isFavorite = false;
+        boolean ignored = false;
         
         HTMLRenderer rend = new HTMLRenderer(I2PAppContext.getGlobalContext());
         SMLParser parser = new SMLParser(I2PAppContext.getGlobalContext());
@@ -436,6 +161,8 @@ public class ViewThreadedServlet extends HttpServlet {
             if (pn.isMember(FilteredThreadIndex.GROUP_FAVORITE)) {
                 isFavorite = true;
             }
+            if (pn.isMember(FilteredThreadIndex.GROUP_IGNORE))
+                ignored = true;
         }
         
         state.incrementRowsWritten();
@@ -498,10 +225,12 @@ public class ViewThreadedServlet extends HttpServlet {
         }
         out.write("</a>\n");
 
-        if (node.getEntry().getKeyHash().equals(user.getBlog())) {
+        if ( (user.getBlog() != null) && (node.getEntry().getKeyHash().equals(user.getBlog())) ) {
             out.write("<img src=\"images/self.png\" alt=\"You wrote this\" border=\"0\" />\n");
         } else if (isFavorite) {
             out.write("<img src=\"images/favorites.png\" alt=\"favorites\" border=\"0\" />\n");
+        } else if (ignored) {
+            out.write("<img src=\"images/addToIgnored.png\" alt=\"ignored\" border=\"0\" />\n");
         } else {
             if (user.getAuthenticated()) {
                 // give them a link to bookmark or ignore the peer
@@ -547,14 +276,8 @@ public class ViewThreadedServlet extends HttpServlet {
         return rendered;
     }
     
-    private String trim(String orig, int maxLen) {
-        if ( (orig == null) || (orig.length() <= maxLen) )
-            return orig;
-        return orig.substring(0, maxLen) + "...";
-    }
-    
     private String getFlagHTML(User user, ThreadNode node) {
-        if (node.containsAuthor(user.getBlog()))
+        if ( (user.getBlog() != null) && (node.containsAuthor(user.getBlog())) )
             return "<img src=\"images/self.png\" border=\"0\" alt=\"You have posted in the thread\" />";
         
         // grab all of the peers in the user's favorites group and check to see if 
@@ -579,282 +302,4 @@ public class ViewThreadedServlet extends HttpServlet {
             return "&nbsp;"; 
     }
     
-    private static final boolean empty(HttpServletRequest req, String param) {
-        String val = req.getParameter(param);
-        return (val == null) || (val.trim().length() <= 0);
-    }
-    
-    private static final boolean empty(String val) {
-        return (val == null) || (val.trim().length() <= 0);
-    }
-    
-    private String getExpandLink(HttpServletRequest req, ThreadNode node) {
-        return getExpandLink(node, req.getRequestURI(), req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
-                             req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), 
-                             req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
-                             req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
-                             req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
-    }
-    private static String getExpandLink(ThreadNode node, String uri, String viewPost, String viewThread, 
-                                        String offset, String tags, String author) {
-        StringBuffer buf = new StringBuffer(64);
-        buf.append(uri);
-        buf.append('?');
-        // expand node == let one of node's children be visible
-        if (node.getChildCount() > 0) {
-            ThreadNode child = node.getChild(0);
-            buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
-            buf.append(child.getEntry().getKeyHash().toBase64()).append('/');
-            buf.append(child.getEntry().getEntryId()).append('&');
-        }
-        
-        if (!empty(viewPost))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
-        else if (!empty(viewThread))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
-        
-        if (!empty(offset))
-            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
-        
-        if (!empty(tags)) 
-            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
-        
-        if (!empty(author)) 
-            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
-        
-        return buf.toString();
-    }
-    private String getCollapseLink(HttpServletRequest req, ThreadNode node) {
-        return getCollapseLink(node, req.getRequestURI(), 
-                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST),
-                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD),
-                               req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
-                               req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
-                               req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
-    }
-
-    private String getCollapseLink(ThreadNode node, String uri, String viewPost, String viewThread, 
-                                   String offset, String tags, String author) { 
-        StringBuffer buf = new StringBuffer(64);
-        buf.append(uri);
-        // collapse node == let the node be visible
-        buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
-        buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
-        buf.append(node.getEntry().getEntryId()).append('&');
-
-        if (!empty(viewPost))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
-        else if (!empty(viewThread))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
-        
-        if (!empty(offset))
-            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
-        
-        if (!empty(tags))
-            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
-        
-        if (!empty(author))
-            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
-        
-        return buf.toString();
-    }
-    private String getProfileLink(HttpServletRequest req, Hash author) {
-        return getProfileLink(author);
-    }
-    private static String getProfileLink(Hash author) { return HTMLRenderer.getMetadataURL(author); }
-    
-    private String getAddToGroupLink(HttpServletRequest req, Hash author, User user, String group) {
-        return getAddToGroupLink(user, author, group, req.getRequestURI(), 
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VISIBLE),
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD),
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), 
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
-    }
-    private String getAddToGroupLink(User user, Hash author, String group, String uri, String visible,
-                                     String viewPost, String viewThread, String offset, String tags, String filteredAuthor) {
-        StringBuffer buf = new StringBuffer(64);
-        buf.append(uri);
-        buf.append('?');
-        if (!empty(visible))
-            buf.append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=').append(visible).append('&');
-        buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_LOCATION).append('=').append(author.toBase64()).append('&');
-        buf.append(ThreadedHTMLRenderer.PARAM_ADD_TO_GROUP_NAME).append('=').append(group).append('&');
-
-        if (!empty(viewPost))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_POST).append('=').append(viewPost).append('&');
-        else if (!empty(viewThread))
-            buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=').append(viewThread).append('&');
-        
-        if (!empty(offset))
-            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
-
-        if (!empty(tags))
-            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
-        
-        if (!empty(filteredAuthor))
-            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(filteredAuthor).append('&');
-        
-        return buf.toString();
-    }
-    private String getViewPostLink(HttpServletRequest req, ThreadNode node, User user, boolean isPermalink) {
-        return ThreadedHTMLRenderer.getViewPostLink(req.getRequestURI(), node, user, isPermalink, 
-                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET), 
-                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
-                                                    req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
-    }
-    private String getViewThreadLink(HttpServletRequest req, ThreadNode node, User user) {
-        return getViewThreadLink(req.getRequestURI(), node, user,
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_OFFSET),
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS),
-                                 req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR));
-    }
-    private static String getViewThreadLink(String uri, ThreadNode node, User user, String offset,
-                                            String tags, String author) {
-        StringBuffer buf = new StringBuffer(64);
-        buf.append(uri);
-        if (node.getChildCount() > 0) {
-            buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
-            ThreadNode child = node.getChild(0);
-            buf.append(child.getEntry().getKeyHash().toBase64()).append('/');
-            buf.append(child.getEntry().getEntryId()).append('&');
-        } else {
-            buf.append('?').append(ThreadedHTMLRenderer.PARAM_VISIBLE).append('=');
-            buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
-            buf.append(node.getEntry().getEntryId()).append('&');
-        }
-        buf.append(ThreadedHTMLRenderer.PARAM_VIEW_THREAD).append('=');
-        buf.append(node.getEntry().getKeyHash().toBase64()).append('/');
-        buf.append(node.getEntry().getEntryId()).append('&');
-        
-        if (!empty(offset))
-            buf.append(ThreadedHTMLRenderer.PARAM_OFFSET).append('=').append(offset).append('&');
-        
-        if (!empty(tags))
-            buf.append(ThreadedHTMLRenderer.PARAM_TAGS).append('=').append(tags).append('&');
-        
-        if (!empty(author))
-            buf.append(ThreadedHTMLRenderer.PARAM_AUTHOR).append('=').append(author).append('&');
-        
-        buf.append("#").append(node.getEntry().toString());
-        return buf.toString();
-    }
-    private String getFilterByTagLink(HttpServletRequest req, ThreadNode node, User user, String tag, String author) { 
-        return ThreadedHTMLRenderer.getFilterByTagLink(req.getRequestURI(), node, user, tag, author);
-    }
-    private String getNavLink(HttpServletRequest req, int offset) {
-        return ThreadedHTMLRenderer.getNavLink(req.getRequestURI(),
-                                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_POST), 
-                                               req.getParameter(ThreadedHTMLRenderer.PARAM_VIEW_THREAD), 
-                                               req.getParameter(ThreadedHTMLRenderer.PARAM_TAGS), 
-                                               req.getParameter(ThreadedHTMLRenderer.PARAM_AUTHOR), 
-                                               offset);
-    }
-    
-    private void renderEnd(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws IOException {
-        out.write(END_HTML);
-    }
-
-    private static final String BEGIN_HTML = "<html>\n" +
-"<head>\n" +
-"<title>Syndie</title>\n" +
-"<style>\n" +
-".overallTable {\n" +
-"	border-spacing: 0px;\n" +
-"	border-width: 0px;\n" +
-"	border: 0px;\n" +
-"	margin: 0px;\n" +
-"	padding: 0px;\n" +
-"}\n" +
-".topNav {\n" +
-"	background-color: #BBBBBB;\n" +
-"}\n" +
-".topNav_user {\n" +
-"	text-align: left;\n" +
-"	float: left;\n" +
-"	align: left;\n" +
-"	display: inline;\n" +
-"}\n" +
-".topNav_admin {\n" +
-"	text-align: right;\n" +
-"	float: right;\n" +
-"	align: right;\n" +
-"	display: inline;\n" +
-"}\n" +
-".controlBar {\n" +
-"	background-color: #BBBBBB;\n" +
-"}\n" +
-".controlBarRight {\n" +
-"	text-align: right;\n" +
-"}\n" +
-".threadEven {\n" +
-"	background-color: #FFFFFF;\n" +
-"	white-space: nowrap;\n" +
-"}\n" +
-".threadOdd {\n" +
-"	background-color: #EEEEEE;\n" +
-"	white-space: nowrap;\n" +
-"}\n" +
-".threadLeft {\n" +
-"	text-align: left;\n" +
-"	align: left;\n" +
-"}\n" +
-".threadRight {\n" +
-"	text-align: right;\n" +
-"}\n" +
-".threadNav {\n" +
-"	background-color: #BBBBBB;\n" +
-"}\n" +
-".threadNavRight {\n" +
-"	text-align: right;\n" +
-"}\n" +
-".postMeta {\n" +
-"	background-color: #BBBBFF;\n" +
-"}\n" +
-".postMetaSubject {\n" +
-"	text-align: left;\n" +
-"}\n" +
-".postMetaLink {\n" +
-"	text-align: right;\n" +
-"}\n" +
-".postDetails {\n" +
-"	background-color: #DDDDFF;\n" +
-"}\n" +
-".postReply {\n" +
-"	background-color: #BBBBFF;\n" +
-"}\n" +
-".postReplyText {\n" +
-"	background-color: #BBBBFF;\n" +
-"}\n" +
-".postReplyOptions {\n" +
-"	background-color: #BBBBFF;\n" +
-"}\n" +
-"</style>\n" +
-"<link href=\"style.jsp\" rel=\"stylesheet\" type=\"text/css\" >\n" +
-"<link href=\"rss.jsp\" rel=\"alternate\" type=\"application/rss+xml\" >\n" +
-"</head>\n" +
-"<body>\n" +
-"<span style=\"display: none\"><a href=\"#bodySubject\">Jump to the beginning of the first post rendered, if any</a>\n" +
-"<a href=\"#threads\">Jump to the thread navigation</a>\n</span>\n" +
-"<table border=\"0\" width=\"100%\" class=\"overallTable\">\n";
-
-   private static final String END_HTML = "</table>\n" +
-"</body>\n";
-   
-    private static class TreeRenderState {
-        private int _rowsWritten;
-        private int _rowsSkipped;
-        private List _ignored;
-        public TreeRenderState(List ignored) { 
-            _rowsWritten = 0; 
-            _rowsSkipped = 0;
-            _ignored = ignored;
-        }
-        public int getRowsWritten() { return _rowsWritten; }
-        public void incrementRowsWritten() { _rowsWritten++; }
-        public int getRowsSkipped() { return _rowsSkipped; }
-        public void incrementRowsSkipped() { _rowsSkipped++; }
-        public List getIgnoredAuthors() { return _ignored; }
-    }
 }
diff --git a/apps/syndie/jsp/web.xml b/apps/syndie/jsp/web.xml
index acdb2e131bc24d0e4a6944319c17309004758301..52c3a854d3689457da1885e7c1abbbb26c663f64 100644
--- a/apps/syndie/jsp/web.xml
+++ b/apps/syndie/jsp/web.xml
@@ -18,6 +18,11 @@
      <servlet-name>net.i2p.syndie.web.ViewThreadedServlet</servlet-name>
      <servlet-class>net.i2p.syndie.web.ViewThreadedServlet</servlet-class>
     </servlet>
+    
+    <servlet>
+     <servlet-name>net.i2p.syndie.web.ProfileServlet</servlet-name>
+     <servlet-class>net.i2p.syndie.web.ProfileServlet</servlet-class>
+    </servlet>
      
     <servlet>
 	 <servlet-name>net.i2p.syndie.UpdaterServlet</servlet-name>
@@ -45,6 +50,10 @@
       <servlet-name>net.i2p.syndie.web.ViewThreadedServlet</servlet-name>
       <url-pattern>/threads.jsp</url-pattern>
     </servlet-mapping>
+    <servlet-mapping> 
+      <servlet-name>net.i2p.syndie.web.ProfileServlet</servlet-name>
+      <url-pattern>/profile.jsp</url-pattern>
+    </servlet-mapping>
     
     <session-config>
         <session-timeout>