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 6ad973cd06068f6a51df0416820ec36e66713c8e..41839f227a1261c4742720ec713bbd645e6a294a 100644
--- a/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java
+++ b/apps/syndie/java/src/net/i2p/syndie/data/BlogInfo.java
@@ -57,6 +57,7 @@ public class BlogInfo {
     public static final String DESCRIPTION = "Description";
     public static final String CONTACT_URL = "ContactURL";
     public static final String EDITION = "Edition";
+    public static final String SUMMARY_ENTRY_ID = "SummaryEntryId";
     
     public void load(InputStream in) throws IOException {
         Log log = I2PAppContext.getGlobalContext().logManager().getLog(getClass());
diff --git a/apps/syndie/java/src/net/i2p/syndie/data/BlogInfoData.java b/apps/syndie/java/src/net/i2p/syndie/data/BlogInfoData.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1aeb60e01f387ef6e9b5218d3f53f08d6b4552c
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/data/BlogInfoData.java
@@ -0,0 +1,127 @@
+package net.i2p.syndie.data;
+
+import java.io.*;
+import java.util.*;
+import net.i2p.client.naming.PetName;
+import net.i2p.data.DataHelper;
+
+/**
+ * Contain the current supplementary data for rendering a blog, as opposed to
+ * just verifying and rendering a post.
+ */
+public class BlogInfoData {
+    private BlogURI _dataEntryId;
+    /** list of List of PetName instances that the blog refers to */
+    private List _referenceGroups;
+    /** customized style config */
+    private Properties _styleOverrides;
+    /** the blog's logo */
+    private Attachment _logo;
+    private List _otherAttachments;
+    
+    public static final String ATTACHMENT_LOGO = "logo.png";
+    public static final String ATTACHMENT_REFERENCE_GROUPS = "groups.txt";
+    public static final String ATTACHMENT_STYLE_OVERRIDE = "style.cfg";
+    /** identifies a post as being a blog info data, not a content bearing post */
+    public static final String TAG = "BlogInfoData";
+
+    public BlogInfoData() {}
+    
+    public BlogURI getEntryId() { return _dataEntryId; }
+    public boolean isLogoSpecified() { return _logo != null; }
+    public Attachment getLogo() { return _logo; }
+    public boolean isStyleSpecified() { return _styleOverrides != null; }
+    public Properties getStyleOverrides() { return _styleOverrides; }
+    public int getReferenceGroupCount() { return _referenceGroups != null ? _referenceGroups.size() : 0; }
+    /** list of PetName elements to be included in the list */
+    public List getReferenceGroup(int groupNum) { return (List)_referenceGroups.get(groupNum); }
+    public int getOtherAttachmentCount() { return _otherAttachments != null ? _otherAttachments.size() : 0; }
+    public Attachment getOtherAttachment(int num) { return (Attachment)_otherAttachments.get(num); }
+    public Attachment getOtherAttachment(String name) {
+        for (int i = 0; i < _otherAttachments.size(); i++) {
+            Attachment a = (Attachment)_otherAttachments.get(i);
+            if (a.getName().equals(name))
+                return a;
+        }
+        return null;
+    }
+    
+    public void writeLogo(OutputStream out) throws IOException {
+        InputStream in = null;
+        try {
+            in = _logo.getDataStream();
+            byte buf[] = new byte[4096];
+            int read = 0;
+            while ( (read = in.read(buf)) != -1)
+                out.write(buf, 0, read);
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+    }
+
+    
+    public void load(EntryContainer entry) throws IOException {
+        _dataEntryId = entry.getURI();
+        Attachment attachments[] = entry.getAttachments();
+        for (int i = 0; i < attachments.length; i++) {
+            if (ATTACHMENT_LOGO.equals(attachments[i].getName())) {
+                _logo = attachments[i];
+            } else if (ATTACHMENT_REFERENCE_GROUPS.equals(attachments[i].getName())) {
+                readReferenceGroups(attachments[i]);
+            } else if (ATTACHMENT_STYLE_OVERRIDE.equals(attachments[i].getName())) {
+                readStyleOverride(attachments[i]);
+            } else {
+                if (_otherAttachments == null)
+                    _otherAttachments = new ArrayList();
+                _otherAttachments.add(attachments[i]);
+            }
+        }
+    }
+    
+    private void readReferenceGroups(Attachment att) throws IOException {
+        InputStream in = null;
+        try {
+            in = att.getDataStream();
+            StringBuffer line = new StringBuffer(128);
+            List groups = new ArrayList();
+            String prevGroup = null;
+            List defaultGroup = new ArrayList();
+            while (true) {
+                boolean ok = DataHelper.readLine(in, line);
+                if (line.length() > 0) {
+                    PetName pn = new PetName(line.toString().trim());
+                    if (pn.getGroupCount() <= 0) {
+                        defaultGroup.add(pn);
+                    } else if (pn.getGroup(0).equals(prevGroup)) {
+                        List curGroup = (List)groups.get(groups.size()-1);
+                        curGroup.add(pn);
+                    } else {
+                        List curGroup = new ArrayList();
+                        curGroup.add(pn);
+                        groups.add(curGroup);
+                        prevGroup = pn.getGroup(0);
+                    }
+                }
+                if (!ok)
+                    break;
+            }
+            if (defaultGroup.size() > 0)
+                groups.add(defaultGroup);
+            _referenceGroups = groups;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+    }
+    
+    private void readStyleOverride(Attachment att) throws IOException {
+        InputStream in = null;
+        try {
+            in = att.getDataStream();
+            Properties props = new Properties();
+            DataHelper.loadProps(props, in);
+            _styleOverrides = props;
+        } finally {
+            if (in != null) try { in.close(); } catch (IOException ioe) {}
+        }
+    }
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/data/EntryContainer.java b/apps/syndie/java/src/net/i2p/syndie/data/EntryContainer.java
index d41a4500471e22f63438445d0ee550773bb65222..a03271e9ac019acc854eeb865077f6cccb0cf773 100644
--- a/apps/syndie/java/src/net/i2p/syndie/data/EntryContainer.java
+++ b/apps/syndie/java/src/net/i2p/syndie/data/EntryContainer.java
@@ -59,7 +59,10 @@ public class EntryContainer {
     public EntryContainer(BlogURI uri, String tags[], byte smlData[]) {
         this();
         _entryURI = uri;
-        _entryData = new Entry(DataHelper.getUTF8(smlData));
+        if ( (smlData == null) || (smlData.length <= 0) )
+            _entryData = new Entry(null);
+        else
+            _entryData = new Entry(DataHelper.getUTF8(smlData));
         setHeader(HEADER_BLOGKEY, Base64.encode(uri.getKeyHash().getData()));
         StringBuffer buf = new StringBuffer();
         for (int i = 0; tags != null && i < tags.length; i++)
@@ -203,10 +206,13 @@ public class EntryContainer {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         ZipOutputStream out = new ZipOutputStream(baos);
         ZipEntry ze = new ZipEntry(ZIP_ENTRY);
-        byte data[] = DataHelper.getUTF8(_entryData.getText());
+        byte data[] = null;
+        if (_entryData.getText() != null)
+            data = DataHelper.getUTF8(_entryData.getText());
         ze.setTime(0);
         out.putNextEntry(ze);
-        out.write(data);
+        if (data != null)
+            out.write(data);
         out.closeEntry();
         for (int i = 0; (_attachments != null) && (i < _attachments.length); i++) {
             ze = new ZipEntry(ZIP_ATTACHMENT_PREFIX + i + ZIP_ATTACHMENT_SUFFIX);
@@ -270,6 +276,9 @@ public class EntryContainer {
             //System.out.println("Read entry [" + name + "] with size=" + entryData.length);
         }
         
+        if (_entryData == null)
+            _entryData = new Entry(null);
+        
         _attachments = new Attachment[attachments.size()];
         
         for (int i = 0; i < attachments.size(); i++) {
diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/BlogRenderer.java b/apps/syndie/java/src/net/i2p/syndie/sml/BlogRenderer.java
new file mode 100644
index 0000000000000000000000000000000000000000..c03c96d1b638b8c819319a63ba2bdb8cd18477dd
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/sml/BlogRenderer.java
@@ -0,0 +1,189 @@
+package net.i2p.syndie.sml;
+
+import java.io.*;
+import java.util.*;
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.PetName;
+import net.i2p.data.*;
+import net.i2p.syndie.data.*;
+import net.i2p.syndie.web.*;
+
+/**
+ * Renders posts for display within the blog view
+ *
+ */
+public class BlogRenderer extends HTMLRenderer {
+    private BlogInfo _blog;
+    private BlogInfoData _data;
+    public BlogRenderer(I2PAppContext ctx, BlogInfo info, BlogInfoData data) {
+        super(ctx);
+        _blog = info;
+        _data = data;
+    }
+    
+    public void receiveHeaderEnd() {
+        _preBodyBuffer.append("<div class=\"syndieBlogPost\"><hr style=\"display: none\" />\n");
+        _preBodyBuffer.append("<div class=\"syndieBlogPostHeader\">\n");
+        _preBodyBuffer.append("<div class=\"syndieBlogPostSubject\">");
+        String subject = (String)_headers.get(HEADER_SUBJECT);
+        if (subject == null)
+            subject = "[no subject]";
+        String tags[] = _entry.getTags();
+        for (int i = 0; (tags != null) && (i < tags.length); i++)
+            displayTag(_preBodyBuffer, _data, tags[i]);
+        _preBodyBuffer.append(getSpan("subjectText")).append(sanitizeString(subject)).append("</span></div>\n");
+        
+        String name = getAuthor();
+        String when = getEntryDate(_entry.getURI().getEntryId());
+        _preBodyBuffer.append("<div class=\"syndieBlogPostFrom\">Posted by: <a href=\"");
+        _preBodyBuffer.append(getMetadataURL(_entry.getURI().getKeyHash()));
+        _preBodyBuffer.append("\" title=\"View their profile\">");
+        _preBodyBuffer.append(sanitizeString(name));
+        _preBodyBuffer.append("</a> on ");
+        _preBodyBuffer.append(when);
+        _preBodyBuffer.append("</div>\n");
+        _preBodyBuffer.append("</div><!-- end syndieBlogPostHeader -->\n");
+        
+        _preBodyBuffer.append("<div class=\"syndieBlogPostSummary\">\n");
+    }
+    
+    public void receiveEnd() { 
+        _postBodyBuffer.append("</div><!-- end syndieBlogPostSummary -->\n");
+        _postBodyBuffer.append("<div class=\"syndieBlogPostDetails\">\n");
+        int childCount = getChildCount(_archive.getIndex().getThreadedIndex().getNode(_entry.getURI()));
+        if ( (_cutReached || childCount > 0) && (_cutBody) ) {
+            _postBodyBuffer.append("<a href=\"");
+            _postBodyBuffer.append(getEntryURL()).append("\" title=\"View comments on this post\">Read more</a> ");
+        }
+        if (childCount > 0) {
+            _postBodyBuffer.append(childCount).append(" ");
+            if (childCount > 1)
+                _postBodyBuffer.append(" comments already, ");
+            else
+                _postBodyBuffer.append(" comment already, ");
+        }
+        _postBodyBuffer.append("<a href=\"");
+        _postBodyBuffer.append(getReplyURL()).append("\" title=\"Reply to this post\">Leave a comment</a>\n");
+        _postBodyBuffer.append("</div><!-- end syndieBlogPostDetails -->\n");
+        _postBodyBuffer.append("</div><!-- end syndieBlogPost -->\n\n");
+    }
+    private int getChildCount(ThreadNode node) {
+        int nodes = 0;
+        for (int i = 0; i < node.getChildCount(); i++) {
+            nodes++;
+            nodes += getChildCount(node.getChild(i));
+        }
+        return nodes;
+    }
+    
+    private String getAuthor() {
+        PetName pn = null;
+        if ( (_entry != null) && (_user != null) )
+            pn = _user.getPetNameDB().getByLocation(_entry.getURI().getKeyHash().toBase64());
+        if (pn != null)
+            return pn.getName();
+        BlogInfo info = null;
+        if (_entry != null) {
+            info = _archive.getBlogInfo(_entry.getURI());
+            if (info != null) {
+                String str = info.getProperty(BlogInfo.NAME);
+                if (str != null)
+                    return str;
+            }
+            return _entry.getURI().getKeyHash().toBase64().substring(0,6);
+        } else {
+            return "No name?";
+        }
+    }
+
+    private void displayTag(StringBuffer buf, BlogInfoData data, String tag) {
+        //buf.append("<a href=\"");
+        //buf.append(getPageURL(_blog.getKey().calculateHash(), tag, -1, null, 5, 0, false, true));
+        //buf.append("\" title=\"Filter the blog by the tag '").append(sanitizeTagParam(tag)).append("'\">");
+        if ( (tag == null) || ("[none]".equals(tag) ) )
+            return;
+        buf.append("<img src=\"").append(getTagIconURL(tag)).append("\" alt=\"");
+        buf.append(sanitizeTagParam(tag)).append("\" />");
+        //buf.append("</a>");
+        buf.append(" ");
+    }
+    
+    public String getMetadataURL(Hash blog) { return ThreadedHTMLRenderer.buildProfileURL(blog); }
+    private String getTagIconURL(String tag) {
+        return "viewicon.jsp?tag=" + Base64.encode(tag) + "&amp;" + 
+               ViewBlogServlet.PARAM_BLOG + "=" + _blog.getKey().calculateHash().toBase64();
+    }
+    
+    private String getReplyURL() { 
+        String subject = (String)_headers.get(HEADER_SUBJECT);
+        if (subject != null) {
+            if (!subject.startsWith("re:"))
+                subject = "re: " + subject;
+        } else {
+            subject = "re: ";
+        }
+        return "post.jsp?" + PostServlet.PARAM_PARENT + "=" 
+               + Base64.encode(_entry.getURI().getKeyHash().toBase64() + "/" + _entry.getURI().getEntryId()) + "&amp;"
+               + PostServlet.PARAM_SUBJECT + "=" + sanitizeTagParam(subject) + "&amp;";
+    }
+    
+    protected String getEntryURL() { return getEntryURL(_user != null ? _user.getShowImages() : true); }
+    protected String getEntryURL(boolean showImages) {
+        if (_entry == null) return "unknown";
+        return "blog.jsp?" 
+               + ViewBlogServlet.PARAM_BLOG + "=" + _blog.getKey().calculateHash().toBase64() + "&amp;"
+               + ViewBlogServlet.PARAM_ENTRY + "="
+               + Base64.encode(_entry.getURI().getKeyHash().getData()) + '/' + _entry.getURI().getEntryId();
+    }
+    
+    protected String getAttachmentURLBase() { 
+        return "invalid";
+    }
+    
+    protected String getAttachmentURL(int id) {
+        if (_entry == null) return "unknown";
+        return "blog.jsp?"
+               + ViewBlogServlet.PARAM_BLOG + "=" + _blog.getKey().calculateHash().toBase64() + "&amp;"
+               + ViewBlogServlet.PARAM_ATTACHMENT + "=" 
+               + Base64.encode(_entry.getURI().getKeyHash().getData()) + "/"
+               + _entry.getURI().getEntryId() + "/" 
+               + id;
+    }
+
+    public String getPageURL(String entry) {
+        StringBuffer buf = new StringBuffer(128);
+        buf.append("blog.jsp?");
+        buf.append(ViewBlogServlet.PARAM_BLOG).append(_blog.getKey().calculateHash().toBase64()).append("&amp;");
+
+        if (entry != null) {
+            if (entry.startsWith("entry://"))
+                entry = entry.substring("entry://".length());
+            else if (entry.startsWith("blog://"))
+                entry = entry.substring("blog://".length());
+            int split = entry.indexOf('/');
+            if (split > 0) {
+                buf.append(ViewBlogServlet.PARAM_ENTRY).append("=");
+                buf.append(sanitizeTagParam(entry.substring(0, split))).append('/');
+                buf.append(sanitizeTagParam(entry.substring(split+1))).append("&amp;");
+            }
+        }
+        return buf.toString();
+    }
+    public String getPageURL(Hash blog, String tag, long entryId, String group, int numPerPage, int pageNum, boolean expandEntries, boolean showImages) {
+        StringBuffer buf = new StringBuffer(128);
+        buf.append("blog.jsp?");
+        buf.append(ViewBlogServlet.PARAM_BLOG).append("=");
+        buf.append(_blog.getKey().calculateHash().toBase64()).append("&amp;");
+        
+        if ( (blog != null) && (entryId > 0) ) {
+            buf.append(ViewBlogServlet.PARAM_ENTRY).append("=");
+            buf.append(blog.toBase64()).append('/');
+            buf.append(entryId).append("&amp;");
+        }
+        if (tag != null)
+            buf.append(ViewBlogServlet.PARAM_TAG).append('=').append(sanitizeTagParam(tag)).append("&amp;");
+        if ( (pageNum >= 0) && (numPerPage > 0) )
+            buf.append(ViewBlogServlet.PARAM_OFFSET).append('=').append(pageNum*numPerPage).append("&amp;");
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/apps/syndie/java/src/net/i2p/syndie/sml/EventReceiverImpl.java b/apps/syndie/java/src/net/i2p/syndie/sml/EventReceiverImpl.java
index 399b01e921acca2c4731bb3f08c5aac14c7bf9d1..854882aa95c22098b97362c092ff1c0aa0315a19 100644
--- a/apps/syndie/java/src/net/i2p/syndie/sml/EventReceiverImpl.java
+++ b/apps/syndie/java/src/net/i2p/syndie/sml/EventReceiverImpl.java
@@ -8,7 +8,7 @@ import net.i2p.util.Log;
  *
  */
 public class EventReceiverImpl implements SMLParser.EventReceiver {
-    private I2PAppContext _context;
+    protected I2PAppContext _context;
     private Log _log;
     
     public EventReceiverImpl(I2PAppContext ctx) {
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 16a10d62c9ac6b978e8c99f8ba717381e4cb7d17..3174f404fd4bef9133cbe5054189778efbf54940 100644
--- a/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java
+++ b/apps/syndie/java/src/net/i2p/syndie/sml/HTMLRenderer.java
@@ -1017,10 +1017,10 @@ public class HTMLRenderer extends EventReceiverImpl {
                _entry.getURI().getEntryId();
     }
 
-    protected String getAttachmentURLBase() { return "viewattachment.jsp"; }
+    protected String getAttachmentURLBase() { return "viewattachment.jsp?"; }
     protected String getAttachmentURL(int id) {
         if (_entry == null) return "unknown";
-        return getAttachmentURLBase() + "?" + 
+        return getAttachmentURLBase() +
                ArchiveViewerBean.PARAM_BLOG + "=" +
                Base64.encode(_entry.getURI().getKeyHash().getData()) +
                "&" + ArchiveViewerBean.PARAM_ENTRY + "=" + _entry.getURI().getEntryId() +
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java b/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java
index 66932e01bbb7951b8b07d1308be1d9e97ce358ad..19af7e9a4dcfce014b92ab290bf590a5bf6ae396 100644
--- a/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ArchiveViewerBean.java
@@ -596,9 +596,11 @@ public class ArchiveViewerBean {
     }
     
     public static void renderAttachment(Map parameters, OutputStream out) throws IOException {
-        Attachment a = getAttachment(parameters);
+        renderAttachment(getAttachment(parameters), out);
+    }
+    public static void renderAttachment(Attachment a, OutputStream out) throws IOException {
         if (a == null) {
-            renderInvalidAttachment(parameters, out);
+            renderInvalidAttachment(out);
         } else {
             InputStream data = a.getDataStream();
             byte buf[] = new byte[1024];
@@ -610,17 +612,21 @@ public class ArchiveViewerBean {
     }
     
     public static final String getAttachmentContentType(Map parameters) {
-        Attachment a = getAttachment(parameters);
-        if (a == null) 
+        return getAttachmentContentType(getAttachment(parameters));
+    }
+    public static final String getAttachmentContentType(Attachment attachment) {
+        if (attachment == null) 
             return "text/html";
-        String mime = a.getMimeType();
+        String mime = attachment.getMimeType();
         if ( (mime != null) && ((mime.startsWith("image/") || mime.startsWith("text/plain"))) )
             return mime;
         return "application/octet-stream";
     }
     
     public static final boolean getAttachmentShouldShowInline(Map parameters) {
-        Attachment a = getAttachment(parameters);
+        return getAttachmentShouldShowInline(getAttachment(parameters));
+    }
+    public static final boolean getAttachmentShouldShowInline(Attachment a) {
         if (a == null) 
             return true;
         String mime = a.getMimeType();
@@ -631,7 +637,9 @@ public class ArchiveViewerBean {
     }
     
     public static final int getAttachmentContentLength(Map parameters) {
-        Attachment a = getAttachment(parameters);
+        return getAttachmentContentLength(getAttachment(parameters));
+    }
+    public static final int getAttachmentContentLength(Attachment a) {
         if (a != null)
             return a.getDataLength();
         else
@@ -639,7 +647,9 @@ public class ArchiveViewerBean {
     }
     
     public static final String getAttachmentFilename(Map parameters) {
-        Attachment a = getAttachment(parameters);
+        return getAttachmentFilename(getAttachment(parameters));
+    }
+    public static final String getAttachmentFilename(Attachment a) {
         if (a != null)
             return a.getName();
         else
@@ -667,7 +677,7 @@ public class ArchiveViewerBean {
         return null;
     }
     
-    private static void renderInvalidAttachment(Map parameters, OutputStream out) throws IOException {
+    private static void renderInvalidAttachment(OutputStream out) throws IOException {
         out.write(DataHelper.getUTF8("<span class=\"b_msgErr\">No such entry, or no such attachment</span>"));
     }
     
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5ecd1c2ddb5277e5ea00f97bfe668d0569339c9
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigBean.java
@@ -0,0 +1,270 @@
+package net.i2p.syndie.web;
+
+import java.io.*;
+import java.util.*;
+import net.i2p.I2PAppContext;
+import net.i2p.client.naming.PetName;
+import net.i2p.data.DataHelper;
+import net.i2p.syndie.*;
+import net.i2p.syndie.data.*;
+import net.i2p.util.Log;
+
+/**
+ *
+ */
+public class BlogConfigBean {
+    private I2PAppContext _context;
+    private Log _log;
+    private User _user;
+    private String _title;
+    private String _description;
+    private String _contactInfo;
+    /** list of list of PetNames */
+    private List _groups;
+    private Properties _styleOverrides;
+    private File _logo;
+    private boolean _loaded;
+    private boolean _updated;
+    
+    public BlogConfigBean() { 
+        _context = I2PAppContext.getGlobalContext();
+        _log = _context.logManager().getLog(BlogConfigBean.class);
+        _groups = new ArrayList();
+        _styleOverrides = new Properties();
+    }
+    
+    public User getUser() { return _user; }
+    public void setUser(User user) { 
+        _user = user;
+        _title = null;
+        _description = null;
+        _contactInfo = null;
+        _groups.clear();
+        _styleOverrides.clear();
+        if (_logo != null)
+            _logo.delete();
+        _logo = null;
+        _loaded = false;
+        _updated = false;
+        load();
+    }
+    public String getTitle() { return _title; }
+    public void setTitle(String title) { 
+        _title = title; 
+        _updated = true;
+    }
+    public String getDescription() { return _description; }
+    public void setDescription(String desc) { 
+        _description = desc; 
+        _updated = true;
+    }
+    public String getContactInfo() { return _contactInfo; }
+    public void setContactInfo(String info) { 
+        _contactInfo = info; 
+        _updated = true;
+    }
+    public int getGroupCount() { return _groups.size(); }
+    /** gets the actual modifiable list of PetName instances */
+    public List getGroup(int i) { return (List)_groups.get(i); }
+    /** gets the actual modifiable list of PetName instances */
+    public List getGroup(String name) {
+        for (int i = 0; i < _groups.size(); i++) {
+            List grp = (List)_groups.get(i);
+            if (grp.size() > 0) {
+                PetName pn = (PetName)grp.get(0);
+                if ( (pn.getGroupCount() == 0) && ( (name == null) || (name.length() <= 0) ) )
+                    return grp;
+                String curGroup = pn.getGroup(0);
+                if (curGroup.equals(name))
+                    return grp;
+            }
+        }
+        return null;
+    }
+    /** adds the given element to the appropriate group (creating a new one if necessary) */
+    public void add(PetName pn) {
+        String groupName = null;
+        if (pn.getGroupCount() > 0)
+            groupName = pn.getGroup(0);
+        List group = getGroup(groupName);
+        if (group == null) {
+            group = new ArrayList(4);
+            group.add(pn);
+            _groups.add(group);
+        } else {
+            group.add(pn);
+        }
+    }
+    public void remove(PetName pn) {
+        String groupName = null;
+        if (pn.getGroupCount() > 0)
+            groupName = pn.getGroup(0);
+        List group = getGroup(groupName);
+        if (group != null) {
+            group.remove(pn);
+            if (group.size() <= 0)
+                _groups.remove(group);
+        }
+    }
+    public void remove(String name) {
+        for (int i = 0; i < getGroupCount(); i++) {
+            List group = getGroup(i);
+            for (int j = 0; j < group.size(); j++) {
+                PetName pn = (PetName)group.get(j);
+                if (pn.getName().equals(name)) {
+                    group.remove(j);
+                    if (group.size() <= 0)
+                        _groups.remove(group);
+                    return;
+                }
+            }
+        }
+    }
+    /** take note that the groups have been updated in some way (reordered, etc) */
+    public void groupsUpdated() { _updated = true; }
+    public String getStyleOverride(String prop) { return _styleOverrides.getProperty(prop); }
+    public void setStyleOverride(String prop, String val) { 
+        _styleOverrides.setProperty(prop, val); 
+        _updated = true;
+    }
+    public void unsetStyleOverride(String prop) { 
+        _styleOverrides.remove(prop); 
+        _updated = true;
+    }
+    public File getLogo() { return _logo; }
+    public void setLogo(File logo) { 
+        if ( (logo != null) && (logo.length() > 128*1024) ) {
+            _log.error("Refusing a logo more than 128KB");
+            return;
+        }
+        _logo = logo; 
+        _updated = true; 
+    }
+    public boolean hasPendingChanges() { return _updated; }
+    
+    private void load() {
+        Archive archive = BlogManager.instance().getArchive();
+        BlogInfo info = archive.getBlogInfo(_user.getBlog());
+        if (info != null) {
+            _title = info.getProperty(BlogInfo.NAME);
+            _description = info.getProperty(BlogInfo.DESCRIPTION);
+            _contactInfo = info.getProperty(BlogInfo.CONTACT_URL);
+            String id = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID);
+            if (id != null) {
+                BlogURI uri = new BlogURI(id);
+                EntryContainer entry = archive.getEntry(uri);
+                if (entry != null) {
+                    BlogInfoData data = new BlogInfoData();
+                    try {
+                        data.load(entry);
+                        if (data.isLogoSpecified()) {
+                            File logo = File.createTempFile("logo", ".png", BlogManager.instance().getTempDir());
+                            FileOutputStream os = null;
+                            try {
+                                os = new FileOutputStream(logo);
+                                data.writeLogo(os);
+                                _logo = logo;
+                            } finally {
+                                if (os != null) try { os.close(); } catch (IOException ioe) {}
+                            }
+                        }
+                        for (int i = 0; i < data.getReferenceGroupCount(); i++) {
+                            List group = (List)data.getReferenceGroup(i);
+                            for (int j = 0; j < group.size(); j++) {
+                                PetName pn = (PetName)group.get(j);
+                                add(pn);
+                            }
+                        }
+                        _styleOverrides.putAll(data.getStyleOverrides());
+                    } catch (IOException ioe) {
+                        _log.warn("Unable to load the blog info data from " + uri, ioe);
+                    }
+                }
+            }
+        }
+        _loaded = true;
+    }
+    
+    public boolean publishChanges() throws IOException {
+        FileInputStream logo = null;
+        try {
+            if (_logo != null)
+                logo = new FileInputStream(_logo);
+            InputStream styleStream = createStyleStream();
+            InputStream groupStream = createGroupStream();
+            
+            String tags = BlogInfoData.TAG;
+            String subject = "n/a";
+            String headers = "";
+            String sml = "";
+            List filenames = new ArrayList();
+            List filestreams = new ArrayList();
+            List filetypes = new ArrayList();
+            if (logo != null) {
+                filenames.add(BlogInfoData.ATTACHMENT_LOGO);
+                filestreams.add(logo);
+                filetypes.add("image/png");
+            }
+            filenames.add(BlogInfoData.ATTACHMENT_STYLE_OVERRIDE);
+            filestreams.add(styleStream);
+            filetypes.add("text/plain");
+            filenames.add(BlogInfoData.ATTACHMENT_REFERENCE_GROUPS);
+            filestreams.add(groupStream);
+            filetypes.add("text/plain");
+            
+            BlogURI uri = BlogManager.instance().createBlogEntry(_user, subject, tags, headers, sml, 
+                                                                 filenames, filestreams, filetypes);
+            if (uri != null) {
+                Archive archive = BlogManager.instance().getArchive();
+                BlogInfo info = archive.getBlogInfo(_user.getBlog());
+                if (info != null) {
+                    String props[] = info.getProperties();
+                    Properties opts = new Properties();
+                    for (int i = 0; i < props.length; i++) {
+                        if (!props[i].equals(BlogInfo.SUMMARY_ENTRY_ID))
+                            opts.setProperty(props[i], info.getProperty(props[i]));
+                    }
+                    opts.setProperty(BlogInfo.SUMMARY_ENTRY_ID, uri.toString());
+                    boolean updated = BlogManager.instance().updateMetadata(_user, _user.getBlog(), opts);
+                    if (updated) {
+                        // ok great, published locally, though should we push it to others?
+                        _log.info("Blog summary updated for " + _user + " in " + uri.toString());
+                        return true;
+                    }
+                } else {
+                    _log.error("Info is not known for " + _user.getBlog().toBase64());
+                    return false;
+                }
+            } else {
+                _log.error("Error creating the summary entry");
+                return false;
+            }
+        } finally {
+            if (logo != null) try { logo.close(); } catch (IOException ioe) {}
+            // the other streams are in-memory, drop with the scope
+        }
+        return false;
+    }
+    private InputStream createStyleStream() throws IOException {
+        StringBuffer buf = new StringBuffer(1024);
+        if (_styleOverrides != null) {
+            for (Iterator iter = _styleOverrides.keySet().iterator(); iter.hasNext(); ) {
+                String key = (String)iter.next();
+                String val = _styleOverrides.getProperty(key);
+                buf.append(key).append('=').append(val).append('\n');
+            }
+        }
+        return new ByteArrayInputStream(DataHelper.getUTF8(buf));
+    }
+    private InputStream createGroupStream() throws IOException {
+        StringBuffer buf = new StringBuffer(1024);
+        for (int i = 0; i < _groups.size(); i++) {
+            List group = (List)_groups.get(i);
+            for (int j = 0; j < group.size(); j++) {
+                PetName pn = (PetName)group.get(j);
+                buf.append(pn.toString()).append('\n');
+            }
+        }
+        return new ByteArrayInputStream(DataHelper.getUTF8(buf));
+    }
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..117bf9161e32496a6816a2e4f4cfec671e1d5f26
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/web/BlogConfigServlet.java
@@ -0,0 +1,231 @@
+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.*;
+
+/**
+ * Display our blog config, and let us edit it through several screens
+ *
+ */
+public class BlogConfigServlet extends BaseServlet {
+    private static final String ATTR_CONFIG_BEAN = "__blogConfigBean";
+    public static final String PARAM_CONFIG_SCREEN = "screen";
+    public static final String SCREEN_GENERAL = "general";
+    public static final String SCREEN_REFERENCES = "references";
+    protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, 
+                                        int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
+        if ( (user == null) || (!user.getAuthenticated() && !BlogManager.instance().isSingleUser())) {
+            out.write("You must be logged in to edit your profile");
+            return;
+        }
+        BlogConfigBean bean = (BlogConfigBean)req.getSession().getAttribute(ATTR_CONFIG_BEAN);
+        if (bean == null) {
+            bean = new BlogConfigBean();
+            bean.setUser(user);
+        }
+        
+        // handle actions here...
+        // on done handling
+        
+        String screen = req.getParameter(PARAM_CONFIG_SCREEN);
+        if (screen == null)
+            screen = SCREEN_GENERAL;
+        out.write("todo: Display screen " + screen);
+        /*
+        if (SCREEN_REFERENCES.equals(screen)) {
+            displayReferencesScreen(req, out, bean);
+        } else {
+            displayGeneralScreen(req, out, bean);
+        }
+         */
+    }   
+    /*
+    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");
+        writeAuthActionFields(out);
+        // 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");
+
+        if (user.getAuthenticated()) {
+            if ( (user.getUsername() == null) || (user.getUsername().equals(BlogManager.instance().getDefaultLogin())) ) {
+                // this is the default user, don't let them change the password
+            } else {
+                out.write("<tr><td colspan=\"3\">Old Password: <input type=\"password\" name=\"oldPassword\" /></td></tr>\n");
+                out.write("<tr><td colspan=\"3\">Password: <input type=\"password\" name=\"password\" /></td></tr>\n");
+                out.write("<tr><td colspan=\"3\">Password again: <input type=\"password\" name=\"passwordConfirm\" /></td></tr>\n");
+            }
+            if (!BlogManager.instance().authorizeRemote(user)) {
+                out.write("<tr><td colspan=\"3\">To access the remote functionality, please specify the administrative password: <br />\n" +
+                          "<input type=\"password\" name=\"adminPass\" /></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 ");
+        PetName pn = user.getPetNameDB().getByLocation(author.toBase64());
+        String name = null;
+        BlogInfo info = archive.getBlogInfo(author);
+        if (pn != null) {
+            out.write(pn.getName());
+            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 {
+            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("<br />\n");
+        out.write("<a href=\"" + getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR 
+                  + '=' + author.toBase64() + "&" + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&\""
+                  + " title=\"View '" + HTMLRenderer.sanitizeTagParam(name) + "'s blog\">View their blog</a> or ");
+        out.write("<a href=\"" + getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR
+                  + '=' + author.toBase64() + "&\">threads they have participated in</a>\n");
+        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 colspan=\"3\"><a href=\"" + remIgnore + "\">Unignore " + pn.getName() + "</a></td></tr>\n");
+            String remCompletely = getRemoveFromGroupLink(user, pn.getName(), "", 
+                                                          baseURI, "", "", "", "", "", author.toBase64());
+            out.write("<tr><td colspan=\"3\"><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 colspan=\"3\"><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 colspan=\"3\"><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 colspan=\"3\"><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 colspan=\"3\"><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");
+                    }
+                }
+            }
+        }
+    }
+     */
+
+    protected String getTitle() { return "Syndie :: Configure blog"; }
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java
index 67fe19171b910ac32b9a8dddb5993575d605969c..6fd9133c47aee6c98ff0166e982b16c05212c19b 100644
--- a/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ProfileServlet.java
@@ -59,7 +59,7 @@ public class ProfileServlet extends BaseServlet {
         out.write("<form action=\"" + baseURI + "\" method=\"POST\">\n");
         writeAuthActionFields(out);
         // now add the form to update
-        out.write("<tr><td colspan=\"3\">Your profile</td></tr>\n");
+        out.write("<tr><td colspan=\"3\">Your profile (<a href=\"configblog.jsp\">configure your blog</a>)</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");
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d5d780c79544af52b248c46f0c33c0ee46aebf9
--- /dev/null
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogServlet.java
@@ -0,0 +1,478 @@
+package net.i2p.syndie.web;
+
+import java.io.*;
+import java.util.*;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+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.*;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+
+/**
+ * Render the appropriate posts for the current blog, using any blog info data available    
+ *
+ */
+public class ViewBlogServlet extends BaseServlet {    
+    public static final String PARAM_OFFSET = "offset";
+    /** $blogHash */
+    public static final String PARAM_BLOG = "blog";
+    /** $blogHash/$entryId */
+    public static final String PARAM_ENTRY = "entry";
+    /** tag,tag,tag */
+    public static final String PARAM_TAG = "tag";
+    /** $blogHash/$entryId/$attachmentId */
+    public static final String PARAM_ATTACHMENT = "attachment";
+    
+    public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+        req.setCharacterEncoding("UTF-8");
+        String attachment = req.getParameter(PARAM_ATTACHMENT);
+        if (attachment != null) {
+            // if they requested an attachment, serve it up to 'em
+            if (renderAttachment(req, resp, attachment))
+                return;
+        }
+        //todo: take care of logo requests, etc
+        super.service(req, resp);
+    }
+    
+    protected void render(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index) throws ServletException, IOException {
+        Archive archive = BlogManager.instance().getArchive();
+
+        Hash blog = null;
+        String name = req.getParameter(PARAM_BLOG);
+        if ( (name == null) || (name.trim().length() <= 0) ) {
+            blog = user.getBlog();
+        } else {
+            byte val[] = Base64.decode(name);
+            if ( (val != null) && (val.length == Hash.HASH_LENGTH) )
+                blog = new Hash(val);
+        }
+        
+        BlogInfo info = null;
+        if (blog != null)
+            info = archive.getBlogInfo(blog);
+        
+        int offset = 0;
+        String off = req.getParameter(PARAM_OFFSET);
+        if (off != null) try { offset = Integer.parseInt(off); } catch (NumberFormatException nfe) {}
+
+        List posts = getPosts(user, archive, info, req, index);
+        render(user, req, out, archive, info, posts, offset);
+    }
+    
+    private BlogURI getEntry(HttpServletRequest req) {
+        String param = req.getParameter(PARAM_ENTRY);
+        if (param != null)
+            return new BlogURI("blog://" + param);
+        return null;
+    }
+    
+    private List getPosts(User user, Archive archive, BlogInfo info, HttpServletRequest req, ThreadIndex index) {
+        List rv = new ArrayList(1);
+        if (info == null) return rv;
+        
+        ArchiveIndex aindex = archive.getIndex();
+        
+        BlogURI uri = getEntry(req);
+        if (uri != null) {
+            rv.add(uri);
+            return rv;
+        }
+        
+        aindex.selectMatchesOrderByEntryId(rv, info.getKey().calculateHash(), null);
+
+        // lets filter out any posts that are not roots
+        for (int i = 0; i < rv.size(); i++) {
+            BlogURI curURI = (BlogURI)rv.get(i);
+            ThreadNode node = index.getNode(curURI);
+            if ( (node != null) && (node.getParent() == null) ) {
+                // ok, its a root
+                Collection tags = node.getTags();
+                if ( (tags != null) && (tags.contains(BlogInfoData.TAG)) ) {
+                    // skip this, as its an info post
+                    rv.remove(i);
+                    i--;
+                }
+            } else {
+                rv.remove(i);
+                i--;
+            }
+        }
+        return rv;
+    }
+    
+    private void render(User user, HttpServletRequest req, PrintWriter out, Archive archive, BlogInfo info, List posts, int offset) throws IOException {
+        String title = null;
+        String desc = null;
+        BlogInfoData data = null;
+        if (info != null) {
+            title = info.getProperty(BlogInfo.NAME);
+            desc = info.getProperty(BlogInfo.DESCRIPTION);
+            String dataURI = info.getProperty(BlogInfo.SUMMARY_ENTRY_ID);
+            if (dataURI != null) {
+                EntryContainer entry = archive.getEntry(new BlogURI(dataURI));
+                if (entry != null) {
+                    data = new BlogInfoData();
+                    try {
+                        data.load(entry);
+                    } catch (IOException ioe) {
+                        data = null;
+                        if (_log.shouldLog(Log.WARN))
+                            _log.warn("Error loading the blog info data from " + dataURI, ioe);
+                    }
+                }
+            }
+        }
+        String pageTitle = "Syndie :: Blogs" + (desc != null ? " :: " + desc : "");
+        if (title != null) pageTitle = pageTitle + " (" + title + ")";
+        pageTitle = HTMLRenderer.sanitizeString(pageTitle);
+        out.write("<html>\n<head>\n<title>" + pageTitle + "</title>\n");
+        out.write("<style>");
+        renderStyle(out, info, data, req);
+        out.write("</style></head>");
+        renderHeader(user, req, out, info, data, title, desc);
+        renderReferences(out, info, data);
+        renderBody(user, out, info, data, posts, offset, archive, req);
+        out.write("</body></html>\n");
+    }
+    private void renderStyle(PrintWriter out, BlogInfo info, BlogInfoData data, HttpServletRequest req) throws IOException {
+        // modify it based on data.getStyleOverrides()...
+        out.write(CSS);
+        Reader css = null;
+        try {
+            InputStream in = req.getSession().getServletContext().getResourceAsStream("/syndie.css");
+            if (in != null) {
+                css = new InputStreamReader(in, "UTF-8");
+                char buf[] = new char[1024];
+                int read = 0;
+                while ( (read = css.read(buf)) != -1) 
+                    out.write(buf, 0, read);
+            }
+        } finally {
+            if (css != null)
+                css.close();
+        }
+        String content = FileUtil.readTextFile("./docs/syndie_standard.css", -1, true); 
+        if (content != null) out.write(content);
+    }
+    
+    private void renderHeader(User user, HttpServletRequest req, PrintWriter out, BlogInfo info, BlogInfoData data, String title, String desc) throws IOException {
+        out.write("<body class=\"syndieBlog\">\n<span style=\"display: none\">" +
+                  "<a href=\"#content\" title=\"Skip to the blog content\">Content</a></span>\n");
+        renderNavBar(user, req, out);
+        out.write("<div class=\"syndieBlogHeader\">\n");
+        if (data != null) {
+            if (data.isLogoSpecified()) {
+                out.write("<img src=\"logo.png\" alt=\"\" />\n");
+            }
+        }
+        String name = desc;
+        if ( (name == null) || (name.trim().length() <= 0) )
+            name = title;
+        if ( ( (name == null) || (name.trim().length() <= 0) ) && (info != null) )
+            name = info.getKey().calculateHash().toBase64();
+        if (name != null) {
+            String url = "blog.jsp?" + (info != null ? PARAM_BLOG + "=" + info.getKey().calculateHash().toBase64() : "");
+            out.write("<b><a href=\"" + url + "\" title=\"Go to the blog root\">" 
+                      + HTMLRenderer.sanitizeString(name) + "</a></b>");
+        }
+        out.write("</div>\n");
+    }
+    
+    private static final String DEFAULT_GROUP_NAME = "References";
+    private void renderReferences(PrintWriter out, BlogInfo info, BlogInfoData data) throws IOException {
+        out.write("<div class=\"syndieBlogLinks\">\n");
+        if (data != null) {
+            for (int i = 0; i < data.getReferenceGroupCount(); i++) {
+                List group = data.getReferenceGroup(i);
+                if (group.size() <= 0) continue;
+                PetName pn = (PetName)group.get(0);
+                String name = null;
+                if (pn.getGroupCount() <= 0)
+                    name = DEFAULT_GROUP_NAME;
+                else
+                    name = HTMLRenderer.sanitizeString(pn.getGroup(0));
+                out.write("<!-- group " + name + " -->\n");
+                out.write("<div class=\"syndieBlogLinkGroup\">\n");
+                out.write("<span class=\"syndieBlogLinkGroupName\">" + name + "</span>\n");
+                out.write("<ul>\n");
+                for (int j = 0; j < group.size(); j++) {
+                    pn = (PetName)group.get(j);
+                    out.write("<li>" + renderLink(pn) + "</li>\n");
+                }
+                out.write("</ul>\n</div>\n<!-- end " + name + " -->\n");
+            }
+        }
+        out.write("<div class=\"syndieBlogLinkGroup\">\n");
+        out.write("<span class=\"syndieBlogLinkGroupName\">Custom links</span>\n");
+        out.write("<ul><li><a href=\"\">are not yet implemented</a></li><li><a href=\"\">but are coming soon</a></li></ul>\n");
+        out.write("</div><!-- end fake group -->");
+        out.write("<div class=\"syndieBlogMeta\">");
+        out.write("Secured by <a href=\"http://syndie.i2p.net/\">Syndie</a>");
+        out.write("</div>\n");
+        out.write("</div><!-- end syndieBlogLinks -->\n\n");
+    }
+    
+    private String renderLink(PetName pn) {
+        return "<a href=\"\" title=\"go somewhere\">" + HTMLRenderer.sanitizeString(pn.getName()) + "</a>";
+    }
+
+    private static final int POSTS_PER_PAGE = 5;
+    private void renderBody(User user, PrintWriter out, BlogInfo info, BlogInfoData data, List posts, int offset, Archive archive, HttpServletRequest req) throws IOException {
+        out.write("<div class=\"syndieBlogBody\">\n<span style=\"display: none\" id=\"content\"></span>\n\n");
+        if (info == null) {
+            out.write("No blog specified\n");
+            return;
+        }
+        
+        BlogRenderer renderer = new BlogRenderer(_context, info, data);
+        
+        if ( (posts.size() == 1) && (req.getParameter(PARAM_ENTRY) != null) ) {
+            BlogURI uri = (BlogURI)posts.get(0);
+            EntryContainer entry = archive.getEntry(uri);
+            renderer.render(user, archive, entry, out, false, true);
+            renderComments(user, out, info, data, entry, archive, renderer);
+        } else {
+            for (int i = offset; i < posts.size() && i < offset + POSTS_PER_PAGE; i++) {
+                BlogURI uri = (BlogURI)posts.get(i);
+                EntryContainer entry = archive.getEntry(uri);
+                renderer.render(user, archive, entry, out, true, true);
+            }
+
+            renderNav(out, info, data, posts, offset, archive, req);
+        }
+
+        out.write("</div><!-- end syndieBlogBody -->\n");
+    }
+    
+    private void renderComments(User user, PrintWriter out, BlogInfo info, BlogInfoData data, EntryContainer entry, 
+                                Archive archive, BlogRenderer renderer) throws IOException {
+        ArchiveIndex index = archive.getIndex();
+        out.write("<div class=\"syndieBlogComments\">\n");
+        renderComments(user, out, entry.getURI(), archive, index, renderer);
+        out.write("</div>\n");
+    }
+    private void renderComments(User user, PrintWriter out, BlogURI parentURI, Archive archive, ArchiveIndex index, BlogRenderer renderer) throws IOException {
+        List replies = index.getReplies(parentURI);
+        if (replies.size() > 0) {
+            out.write("<ul>\n");
+            for (int i = 0; i < replies.size(); i++) {
+                BlogURI uri = (BlogURI)replies.get(i);
+                out.write("<li>");
+                if (!shouldIgnore(user, uri)) {
+                    EntryContainer cur = archive.getEntry(uri);
+                    renderer.render(user, archive, cur, out, false, true);
+                    // recurse
+                    renderComments(user, out, uri, archive, index, renderer);
+                }
+                out.write("</li>\n");
+            }
+            out.write("</ul>\n");
+        }
+    }
+    
+    private boolean shouldIgnore(User user, BlogURI uri) {
+        PetName pn = user.getPetNameDB().getByLocation(uri.getKeyHash().toBase64());
+        return ( (pn != null) && pn.isMember(FilteredThreadIndex.GROUP_IGNORE));
+    }
+    
+    private void renderNav(PrintWriter out, BlogInfo info, BlogInfoData data, List posts, int offset, Archive archive, HttpServletRequest req) throws IOException {
+        out.write("<div class=\"syndieBlogNav\"><hr style=\"display: none\" />\n");
+        String uri = req.getRequestURI() + "?";
+        if (info != null)
+            uri = uri + PARAM_BLOG + "=" + info.getKey().calculateHash().toBase64() + "&amp;";
+        if (offset + POSTS_PER_PAGE >= posts.size())
+            out.write(POSTS_PER_PAGE + " more older entries");
+        else
+            out.write("<a href=\"" + uri + "offset=" + (offset+POSTS_PER_PAGE) + "\">" 
+                      + POSTS_PER_PAGE + " older entries</a>");
+        out.write(" | ");
+        if (offset <= 0)
+            out.write(POSTS_PER_PAGE + " more recent entries");
+        else
+            out.write("<a href=\"" + uri + "offset=" + 
+                      (offset >= POSTS_PER_PAGE ? offset-POSTS_PER_PAGE : 0) 
+                      + "\">" + POSTS_PER_PAGE + " more recent entries</a>");
+        
+        out.write("</div><!-- end syndieBlogNav -->\n");
+    }
+
+    /** 
+     * render the attachment to the browser, using the appropriate mime types, etc
+     * @param attachment formatted as $blogHash/$entryId/$attachmentId
+     * @return true if rendered 
+     */
+    private boolean renderAttachment(HttpServletRequest req, HttpServletResponse resp, String attachment) throws ServletException, IOException {
+        int split = attachment.lastIndexOf('/');
+        if (split <= 0)
+            return false;
+        BlogURI uri = new BlogURI("blog://" + attachment.substring(0, split));
+        try { 
+            int attachmentId = Integer.parseInt(attachment.substring(split+1)); 
+            if (attachmentId < 0) return false;
+            EntryContainer entry = BlogManager.instance().getArchive().getEntry(uri);
+            if (entry == null) {
+                System.out.println("Could not render the attachment [" + uri + "] / " + attachmentId);
+                return false;
+            }
+            Attachment attachments[] = entry.getAttachments();
+            if (attachmentId >= attachments.length) {
+                System.out.println("Out of range attachment on " + uri + ": " + attachmentId);
+                return false;
+            }
+            
+            resp.setContentType(ArchiveViewerBean.getAttachmentContentType(attachments[attachmentId]));
+            boolean inline = ArchiveViewerBean.getAttachmentShouldShowInline(attachments[attachmentId]);
+            String filename = ArchiveViewerBean.getAttachmentFilename(attachments[attachmentId]);
+            if (inline)
+                resp.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\"");
+            else
+                resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
+            int len = ArchiveViewerBean.getAttachmentContentLength(attachments[attachmentId]);
+            if (len >= 0)
+                resp.setContentLength(len);
+            ArchiveViewerBean.renderAttachment(attachments[attachmentId], resp.getOutputStream());
+            return true;
+        } catch (NumberFormatException nfe) {}
+        return false;
+    }
+    
+    private static final String CSS = 
+"<style>\n" +
+"body {\n" +
+"	margin: 0px;\n" +
+"	padding: 0px;\n" +
+"	font-family: Arial, Helvetica, sans-serif;\n" +
+"}\n" +
+".syndieBlog {\n" +
+"	font-size: 100%;\n" +
+"	margin: 0px;\n" +
+"	border: 0px;\n" +
+"	padding: 0px;\n" +
+"	border-width: 0px;\n" +
+"	border-spacing: 0px;\n" +
+"}\n" +
+".syndieBlogTopNav {\n" +
+"	width: 100%;\n" +
+"	height: 20px;\n" +
+"	background-color: #BBBBBB;\n" +
+"}\n" +
+".syndieBlogTopNavUser {\n" +
+"	text-align: left;\n" +
+"	float: left;\n" +
+"	display: inline;\n" +
+"}\n" +
+".syndieBlogTopNavAdmin {\n" +
+"	text-align: left;\n" +
+"	float: right;\n" +
+"	display: inline;\n" +
+"}\n" +
+".syndieBlogHeader {\n" +
+"	width: 100%;\n" +
+"	height: 50px;\n" +
+"	font-size: 120%;\n" +
+"	background-color: black;\n" +
+"	color: white;\n" +
+"}\n" +
+".syndieBlogLinks {\n" +
+"	width: 200px;\n" +
+"}\n" +
+".syndieBlogLinkGroup {\n" +
+"	text-align: left;\n" +
+"	font-size: 80%;\n" +
+"	background-color: #DDD;\n" +
+"	border: solid;\n" +
+"	//border-width: 5px 5px 0px 5px;\n" +
+"	//border-color: #FFFFFF;\n" +
+"	border-width: 1px 1px 1px 1px;\n" +
+"	border-color: #000;\n" +
+"	margin-top: 5px;\n" +
+"	margin-right: 5px;\n" +
+"}\n" +
+".syndieBlogLinkGroup ul {\n" +
+"	list-style: none;\n" +
+"	margin-left: 0;\n" +
+"	margin-top: 0;\n" +
+"	margin-bottom: 0;\n" +
+"	padding-left: 0;\n" +
+"}\n" +
+".syndieBlogLinkGroup li {\n" +
+"	margin: 0;\n" +
+"}\n" +
+".syndieBlogLinkGroup li a {\n" +
+"	display: block;\n" +
+"	width: 100%;\n" +
+"}\n" +
+".syndieBlogLinkGroupName {\n" +
+"	font-size: 80%;\n" +
+"	font-weight: bold;\n" +
+"}\n" +
+".syndieBlogMeta {\n" +
+"	text-align: left;\n" +
+"	font-size: 80%;\n" +
+"	background-color: #DDD;\n" +
+"	border: solid;\n" +
+"	border-width: 1px 1px 1px 1px;\n" +
+"	border-color: #000;\n" +
+"                   width: 90%;\n" +
+"	margin-top: 5px;\n" +
+"	margin-right: 5px;\n" +
+"}\n" +
+".syndieBlogBody {\n" +
+"	position: absolute;\n" +
+"	top: 70px;\n" +
+"	left: 200px;\n" +
+"	float: left;\n" +
+"}\n" +
+".syndieBlogPost {\n" +
+"	border: solid;\n" +
+"	border-width: 1px 1px 1px 1px;\n" +
+"	border-color: #000;\n" +
+"	margin-top: 5px;\n" +
+"	width: 100%;\n" +
+"}\n" +
+".syndieBlogPostHeader {\n" +
+"	background-color: #BBB;\n" +
+"}\n" +
+".syndieBlogPostSubject {\n" +
+"	text-align: left;\n" +
+"}\n" +
+".syndieBlogPostFrom {\n" +
+"	text-align: right;\n" +
+"}\n" +
+".syndieBlogPostSummary {\n" +
+"	background-color: #FFFFFF;\n" +
+"}\n" +
+".syndieBlogPostDetails {\n" +
+"	background-color: #DDD;\n" +
+"}\n" +
+".syndieBlogNav {\n" +
+"	text-align: center;\n" +
+"}\n" +
+".syndieBlogComments {\n" +
+"                   border: none;\n" +
+"                   margin-top: 5px;\n" +
+"                   margin-left: 0px;\n" +
+"                   float: left;\n" +
+"}\n" +
+".syndieBlogComments ul {\n" +
+"                   list-style: none;\n" +
+"                   margin-left: 10;\n" +
+"                   padding-left: 0;\n" +
+"}\n";
+
+    protected String getTitle() { return "unused"; }
+    protected void renderServletDetails(User user, HttpServletRequest req, PrintWriter out, ThreadIndex index, 
+                                        int threadOffset, BlogURI visibleEntry, Archive archive) throws IOException {
+        throw new RuntimeException("unused");
+    }
+}
diff --git a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java
index 6f09e4153dfee523bd243ac2d5fad166ba176a19..80f28402dc672b0d797e0fd23d6ac2075047f2d9 100644
--- a/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java
+++ b/apps/syndie/java/src/net/i2p/syndie/web/ViewBlogsServlet.java
@@ -29,8 +29,9 @@ public class ViewBlogsServlet extends BaseServlet {
         if ( (lastPost > 0) && (dayBegin - 3*24*60*60*1000l >= lastPost) ) // last post was old 3 days ago
             daysAgo = (int)((dayBegin - lastPost + 24*60*60*1000l-1)/(24*60*60*1000l));
         daysAgo++;
-        return getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + blog.toBase64()
-               + '&' + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&daysBack=" + daysAgo;
+        return "blog.jsp?" + ViewBlogServlet.PARAM_BLOG + "=" + blog.toBase64();
+        //return getControlTarget() + "?" + ThreadedHTMLRenderer.PARAM_AUTHOR + '=' + blog.toBase64()
+        //       + '&' + ThreadedHTMLRenderer.PARAM_THREAD_AUTHOR + "=true&daysBack=" + daysAgo;
     }
     
     private String getPostDate(long when) {
diff --git a/apps/syndie/jsp/web.xml b/apps/syndie/jsp/web.xml
index 6a57e60a383d2684fa15ec9278c3a7f1796277e1..3da3b8bd36702581d29bb73651a4f9cbdf6190df 100644
--- a/apps/syndie/jsp/web.xml
+++ b/apps/syndie/jsp/web.xml
@@ -69,6 +69,16 @@
      <servlet-class>net.i2p.syndie.web.ViewBlogsServlet</servlet-class>
     </servlet>
      
+    <servlet>
+     <servlet-name>net.i2p.syndie.web.BlogConfigServlet</servlet-name>
+     <servlet-class>net.i2p.syndie.web.BlogConfigServlet</servlet-class>
+    </servlet>
+     
+    <servlet>
+     <servlet-name>net.i2p.syndie.web.ViewBlogServlet</servlet-name>
+     <servlet-class>net.i2p.syndie.web.ViewBlogServlet</servlet-class>
+    </servlet>
+     
     <servlet>
 	 <servlet-name>net.i2p.syndie.UpdaterServlet</servlet-name>
 	 <servlet-class>net.i2p.syndie.UpdaterServlet</servlet-class>
@@ -135,6 +145,14 @@
       <servlet-name>net.i2p.syndie.web.ViewBlogsServlet</servlet-name>
       <url-pattern>/blogs.jsp</url-pattern>
     </servlet-mapping>
+    <servlet-mapping> 
+      <servlet-name>net.i2p.syndie.web.BlogConfigServlet</servlet-name>
+      <url-pattern>/configblog.jsp</url-pattern>
+    </servlet-mapping>
+    <servlet-mapping> 
+      <servlet-name>net.i2p.syndie.web.ViewBlogServlet</servlet-name>
+      <url-pattern>/blog.jsp</url-pattern>
+    </servlet-mapping>
     
     <session-config>
         <session-timeout>
diff --git a/history.txt b/history.txt
index 4b43e3ceb068b3d7f12080f7e6ff04bc42f6909b..c2d67c55320580eb79c61e7d6ff3bad7acacb100 100644
--- a/history.txt
+++ b/history.txt
@@ -1,4 +1,8 @@
-$Id: history.txt,v 1.377 2006/01/01 12:23:29 jrandom Exp $
+$Id: history.txt,v 1.378 2006/01/04 21:48:17 jrandom Exp $
+
+2006-01-08  jrandom
+    * First pass of the new blog interface, though without much of the useful
+      customization features (coming soon)
 
 2006-01-04  jrandom
     * Rather than profile individual tunnels for throughput over their