constance

Scripts for generating (an earlier obsolete version of) my personal web site
git clone https://code.djc.id.au/git/constance/
commit da060dac6deb9343afbe6dce452544eaebfa9e59
parent 4a662eab79c964eac6ba4f6d0a6624d16553161e
Author: Dan Callaghan <djc@djc.id.au>
Date:   Tue,  2 Sep 2008 19:27:25 +1000

redesign based on http://diveintomark.org/archives/2008/06/21/minimalism

* simpler CSS
* fixed bug with next-page links
* cover images for reading log entries

Diffstat:
Mapp.py | 32+++++++++++++++++++++++---------
Mblog.py | 1+
Mconfig.py | 1+
Mstatic/css/common.css | 497++++++++++++++++---------------------------------------------------------------
Dstatic/images/bg.gif | 0
Dstatic/images/book_open.png | 0
Dstatic/images/comm.gif | 0
Astatic/images/star-half.png | 0
Astatic/images/star-off.png | 0
Dstatic/images/tag_blue.png | 0
Mtemplates/_commonwrapper.xml | 107+++++++++++++++++++++++++++++++++----------------------------------------------
Mtemplates/_entry.xml | 107+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtemplates/multiple.xml | 16++++++----------
Mtemplates/multiple_atom.xml | 20++++++++------------
14 files changed, 239 insertions(+), 542 deletions(-)
diff --git a/app.py b/app.py
@@ -29,7 +29,7 @@ class BlogApplication(RegexApplication):
 
 	def index(self):
 		offset = int(self.request.args.get('offset', 0))
-		sorted_entries = sorted(self.entries, key=lambda e: e.publication_date, reverse=True)[offset:offset + config.ENTRIES_PER_PAGE]
+		sorted_entries = sorted(self.entries, key=lambda e: e.publication_date, reverse=True)
 		format = self.request.args.get('format', 'html')
 		if format == 'html':
 			rendered = template_loader.load('multiple.xml').generate(
@@ -40,7 +40,9 @@ class BlogApplication(RegexApplication):
 			return HttpResponse(rendered, [('Content-Type', 'text/html')], 200)
 		elif format == 'atom':
 			rendered = template_loader.load('multiple_atom.xml').generate(
-					sorted_entries=sorted_entries, 
+					title=None, 
+					url=config.ABS_BASE + '/', 
+					sorted_entries=sorted_entries[:config.ENTRIES_PER_PAGE], 
 					feed_updated=sorted_entries[0].modified_date
 					).render('xml')
 			return HttpResponse(rendered, [('Content-Type', 'application/atom+xml')], 200)
@@ -65,13 +67,25 @@ class BlogApplication(RegexApplication):
 			raise PageNotFound()
 		offset = int(self.request.args.get('offset', 0))
 		entries = by_tag[tag]
-		sorted_entries = sorted(entries, key=lambda e: e.publication_date, reverse=True)[offset:offset + config.ENTRIES_PER_PAGE]
-		rendered = template_loader.load('multiple.xml').generate(
-				title=u'“%s” tag' % tag, 
-				sorted_entries=sorted_entries, 
-				offset=offset
-				).render('xhtml')
-		return HttpResponse(rendered, [('Content-Type', 'text/html')], 200)
+		sorted_entries = sorted(entries, key=lambda e: e.publication_date, reverse=True)
+		format = self.request.args.get('format', 'html')
+		if format == 'html':
+			rendered = template_loader.load('multiple.xml').generate(
+					title=u'“%s” tag' % tag, 
+					sorted_entries=sorted_entries, 
+					offset=offset
+					).render('xhtml')
+			return HttpResponse(rendered, [('Content-Type', 'text/html')], 200)
+		elif format == 'atom':
+			rendered = template_loader.load('multiple_atom.xml').generate(
+					title=u'“%s” tag' % tag, 
+					url='%s/+tags/%s' % (config.ABS_BASE, tag.encode(self.charset)), 
+					sorted_entries=sorted_entries[:config.ENTRIES_PER_PAGE], 
+					feed_updated=sorted_entries[0].modified_date
+					).render('xml')
+			return HttpResponse(rendered, [('Content-Type', 'application/atom+xml')], 200)
+		else:
+			raise PageNotFound('Unknown format %r' % format)
 
 
 app = BlogApplication
diff --git a/blog.py b/blog.py
@@ -123,6 +123,7 @@ class ReadingLogEntry(object):
 		self.author = yaml_dict['Author']
 		self.publication_date = self.modified_date = self.date = yaml_dict['Date']
 		self.url = yaml_dict.get('URL', None)
+		self.isbn = yaml_dict.get('ISBN', None)
 		self.rating = yaml_dict.get('Rating', None)
 		self.tags = frozenset()
 		self._guid = yaml_dict.get('GUID', None)
diff --git a/config.py b/config.py
@@ -11,3 +11,4 @@ BLOG_NAME = u'djc'
 BLOG_AUTHOR = u'djc'
 BLOG_EMAIL = None
 ENTRIES_PER_PAGE = 20
+LIBRARYTHING_DEVKEY = 'f6da4b15120267233430bb13cdaf1be9'
diff --git a/static/css/common.css b/static/css/common.css
@@ -1,432 +1,135 @@
-*
-{
-	margin: 0;
-	padding: 0;
-}
-body
-{
-	border-top:10px solid #0D0F10;
-	background: #282C2F url(../images/bg.gif) repeat-y left;
-	color: #fff;
-	font-family: "Lucida Grande", "Lucida Sans Unicode", Tahoma, Helvetica, Verdana, sans-serif;
-	margin: 0;
-	padding: 0;
-}
-p {
-	margin: 1.1em 0;
-}
-div.comment-number {
-	float: right;
-	color: #999;
-	width: 40px;
-	text-align: right;
-}
-.class_comment1 { background: #E9E9EA; border: 1px solid #E0DEDE; }
-.class_comment2 { background: #F4F3F3; border: 1px solid #E0DEDE; }
-#leftwrap
-{
-	background-color: #202326;
-	display: inline;
-	float: left;
-	margin: 0;
-	width: 250px;
-	padding: 20px;
-	overflow: hidden;
-	position: relative;
-	text-align: right;
-	font-size: smaller;
-}
-.navwidth
-{
-	overflow: hidden;
-}
-#adsense
-{
-	padding: 0 0 10px;
-}
-h1#blogname
-{
-	color: #C7FF68;
-	font-size: 2em;
-}
 
-#content
-{
-	background: #282C2F;
-	color: #fff;
-	display: inline;
-	float: left;
-	padding: 1.5em;
-	max-width: 40em;
-	width: auto !important;
-	width: 40em;
-	line-height: 2.7ex;
-}
-.alert
-{
-	background: #FFF6BF;
-	border-bottom: 2px solid #FFD324;
-	border-top: 2px solid #FFD324;
-	margin: 10px auto;
-	padding: 5px 20px 5px 20px;
-}
-#sidebar
-{
-	width: 250px;
-	display: inline;
-	float: left;
-	margin-top: 15px;
-	text-align: right;
-}
-#footer
-{
-	clear: both;
-	margin: 0 auto;
-	background:#0D0F10;
-	padding: 1.75em;
-	text-align: center;
-}
-
-h1, h2, h3, h4 {
-	line-height: 2.3ex;
-}
-h2
-{
-	color: #C7FF68;
-}
-h4
-{
-	color: #C7FF68;
-}
-.themes
-{
-	padding: 25px 0;
+body {
+    font-family: "Cambria", "Liberation Serif", serif;
+    line-height: 2.75ex;
+    margin: 0;
+    padding: 0;
+    background-color: #ddd;
 }
-ol#commentlist
-{
-	list-style: none;
-	padding: 0;
+#contentwrapper {
+    border-bottom: 1px dashed #aaa;
+    background-color: white;
+    margin: 0;
+    padding: 0.001em 1em;
 }
-.commenttext
-{
-	background: #303538 url(../images/comm.gif) no-repeat left top;
-	min-height: 90px;
-}
-.commenttext .authorcomment
-{
-	background: #202326;
-	margin-top: 3px;
-	min-height: 90px;
-	padding: 10px;
-}
-* html .commenttext
-{
-	height: 90px;
-	overflow: visible;
-}
-.commentp
-{
-	margin-left: 5px;
-	padding: 21px 12px 10px 10px;
-}
-.commenttext p
-{
-	margin: 0 0 15px;
-	padding: 0;
-}
-#commentblock ol li
-{
-	margin-bottom: 30px;
-}
-.gravatar
-{
-	background:#202326;
-	display: inline;
-	float: left;
-	height: 40px;
-	margin: 24px 0 0 9px;
-	padding: 5px;
-	width: 40px;
+#content {
+    margin: 1em auto;
+    max-width: 45em;
 }
 
-#commentsform p
-{
-	margin-bottom: 5px;
-	margin-top: 5px;
-}
-a img
-{
-	border: 0;
-}
-h3.entrytitle,h3
-{
-	color: #fff;
-	display: block;
-	font-size: 1.75em;
-	font-weight: 400;
-	margin: 0;
-	padding-bottom: 3px;
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Corbel", "Liberation Sans", sans-serif;
 }
-h3.entrytitle a,h3.entrytitle a:visited,h3 a
-{
-	color: #fff;
-	font-weight: 400;
-	text-decoration: none;
-}
-h3.entrytitle a:hover,h3 a:hover
-{
-	color: #C7FF68;
-	text-decoration: none;
-}
-.entry
-{
-	padding: 1em 0 0 0;
-}
-.readinglog h3.entrytitle {
-	font-size: 1.25em;
-}
-.readinglog h3.entrytitle a {
-	font-style: italic;
-}
-.entrybody img
-{
 
+span.inlinesep {
+    font-weight: bold;
+    padding: 0 0.25em;
 }
-.entrybody p
-{
-	text-align: left;
-}
-.entrybody
-{
-	border-bottom: 1px solid #202326;
+div.blocksep {
+    font-size: 1.6em;
+    text-align: center;
 }
 
-.entrymeta
-{
-	color: #737B81;
-	margin: 0.2em 0;
+.entry {
+    margin: 3em 0;
+    clear: both;
 }
-.entrymeta-single
-{
-	color: #737B81;
-	margin: 0.5em 0;
+.entry h3.entrytitle {
+    display: inline;
+    font-size: 1.5em;
+    text-transform: uppercase;
+    letter-spacing: 0.1ex;
+    word-spacing: 0.2em;
+    color: black;
+    margin-right: 0.5em;
 }
-h2.archives
-{
-	color: #737B81;
-	font-style: italic;
-	margin-bottom: 1em;
-	text-align: left;
+.entry .entrydate {
+    font-family: "Corbel", "Liberation Sans", sans-serif;
+    display: inline;
+    font-size: 1.2em;
+    letter-spacing: 0.1ex;
+    word-spacing: 0.1em;
+    color: #888;
+    white-space: nowrap;
+    margin-right: 0.25em;
 }
-#sidebar ul#feed
-{
-	list-style: none;
-	margin: 15px 0;
-	padding: 0;
+.entry .entrydate a {
+    color: #888;
 }
-#sidebar ul#feed li
-{
-	padding-bottom: 3px;
-	padding-top: 3px;
+.entry .entrytags {
+    display: inline;
+    font-style: italic;
+    color: #888;
 }
-#sidebar ul#feed li a
-{
-	background: url(../images/feed-icon-16x16.png) no-repeat left 50%;
-	border-style: none;
-	padding-left: 25px;
+.entry .entrytags a {
+    color: black;
+    white-space: nowrap;
 }
-.entrybody ul,.entrybody ol
-{
-	margin-bottom: 10px;
-	margin-left: 30px;
-	margin-top: 10px;
+.entry .entrycommentslink {
+    text-align: right;
+    font-style: italic;
 }
-#commentlink a:link, #commentlink a:visited {
-	color:#C7FF68;
+.entry .entrycommentslink a {
+    color: #888;
+    white-space: nowrap;
 }
 
-.entrybody li
-{
-	padding-bottom: 2px;
-	padding-top: 2px;
+/* Comments */
+.entry .commentblock {
+    margin: 2em 4em 0 4em;
+    font-size: 0.9em;
 }
-.entrybody ul li
-{
-	list-style:square;
+.entry .commentblock h4 {
+    font-size: 1.2em;
 }
-#sidebar h2
-{
-	color: #6CF;
-	font-size: 1em;
-	font-weight: normal;
-	margin: 0 0 5px 0;
-	text-transform: uppercase;
+.entry .commentblock .commentmeta {
+    text-align: right;
 }
-#sidebar h2 a
-{
-	color: #6CF;
+.entry .commentblock .commentmeta .commentdatetime {
+    color: #888;
+    margin: 0 0.5em;
 }
-.sidebarbg
-{
-	background-color: #eee;
-}
-#sidebar p
-{
-	margin-bottom: 10px;
-	margin-top: 10px;
-}
-#sidebar ul
-{
-	list-style: none;
-	margin: 0 0 20px 0;
-}
-#sidebar li
-{
-	list-style: none;
-}
-#sidebar li a:link, #sidebar li a:visited
-{
-color:#fff;
-}
-#sidebar li a:hover, #sidebar li a:active
-{
-color:#C7FF68;
-}
-
-
-
-#searchdiv
-{
-font-family:"Lucida Grande","Lucida Sans Unicode",Tahoma, Helvetica, Verdana, sans-serif;
-	padding: 0;
-}
-
-#searchdiv input
-{
-background:#181B1C;
-font-family:"Lucida Grande","Lucida Sans Unicode",Tahoma, Helvetica, Verdana, sans-serif;
-border:1px solid #181B1C;
+.entry .commentblock .commentmeta .permalink {
+    color: #888;
 }
 
-.navigation
-{
-	padding-top: 1px;
-	text-transform:uppercase;
-	text-align: right;
-}
-.navigation a:link, .navigation a:visited
-{
-	color: #3E4449;
-	font-size: 1.5em;
-	list-style: none;
-	margin: 0;
-}
-.navigation a:hover
-{
-	color: #737B81;
-	list-style: none;
-	margin: 0;
-	padding: 2px 0;
-	text-decoration: none;
-}
-.current_page_item a:link,.current_page_item a:visited
-{
-	color: #C7FF68;
-	list-style: none;
-	margin: 0;
-	padding: 2px 0;
-	text-decoration: none;
-}
-.current_page_item a:hover
-{
-	list-style: none;
-	margin: 0;
-	padding: 2px 0;
-	text-decoration: none;
-}
-.navigation ul
-{
-	list-style: none;
-	margin: 0;
-	padding: 0;
-}
-.navigation li
-{
-	list-style: none;
-	margin: 0;
-	padding: 0;
-padding-bottom:2px;
-}
-blockquote
-{
-	border-left: 2px solid #535B61;
-	color: #535B61;
-	font-style: italic;
-	margin: 0 25px;
-	padding-left: 8px;
-}
-h1,h2,h3,h4,#comments
-{
-	font-weight: 400;
-}
-h3,#commentblock h2
-{
-	padding-bottom: 20px;
-}
-.entrybody a:link,.entrybody a:visited
-{
-	color: #6CF;
-	font-weight: 400;
-	text-decoration: none;
-}
-.entrybody a:active,.entrybody a:hover
-{
-	color: #6CF;
-	font-weight: 400;
-	text-decoration: underline;
-}
-a,a:visited,a:hover
-{
-	color: #6CF;
-	text-decoration: none;
-}
-
-a:hover
-{
-	text-decoration: underline;
+/* Reading log entries */
+.readinglog h3.entrytitle {
+    font-size: 1.2em;
+    word-spacing: 0.1em;
+    letter-spacing: 0;
+    text-transform: none;
 }
-.subscribe img
-{
-	padding-bottom: 3px;
-	padding-right: 3px;
+.readinglog h3.entrytitle a {
+    font-style: italic;
+    margin-right: 0.2em;
 }
-#rssicon
-{
-	padding-bottom: 10px;
+.readinglog .author {
+    white-space: nowrap;
 }
-.technorati {
-display:none;
+.readinglog img.cover {
+    float: right;
+    margin: 0 0 1em 1em;
 }
-
-ul.searchbg {
-background-color:#fff;
+.readinglog .rating {
+    display: inline;
+    white-space: nowrap;
 }
 
-code {
-	font-family: monospace;
+/* Links at the bottom */
+#links {
+    padding: 0.5em 2em 2em 2em;
+    margin-top: 1em;
 }
-pre {
-	margin: 0 2em;
-	padding: 0.5em 1em;
+.linkrow {
+    text-align: center;
+    margin: 1em 0;
 }
-
-p.UTWPrimaryTags {
-}
-p.UTWPrimaryTags img {
-	margin-right: 0.3em;
+.linkrow h2 {
+    display: inline;
+    font-size: 1.1em;
+    margin-right: 1em;
 }
-
-.readinglog h3 {
-	padding: 0 0 0 22px;
-	background: url(../images/book_open.png) no-repeat center left;
+.linkrow p {
+    display: inline;
 }
diff --git a/static/images/bg.gif b/static/images/bg.gif
Binary files differ.
diff --git a/static/images/book_open.png b/static/images/book_open.png
Binary files differ.
diff --git a/static/images/comm.gif b/static/images/comm.gif
Binary files differ.
diff --git a/static/images/star-half.png b/static/images/star-half.png
Binary files differ.
diff --git a/static/images/star-off.png b/static/images/star-off.png
Binary files differ.
diff --git a/static/images/tag_blue.png b/static/images/tag_blue.png
Binary files differ.
diff --git a/templates/_commonwrapper.xml b/templates/_commonwrapper.xml
@@ -8,80 +8,63 @@
 import config
 ?>
 
+<span py:def="inline_sep()" class="inlinesep">·</span>
+<div py:def="block_sep()" class="blocksep">~</div>
+
 <py:match path="head">
 	<head profile="http://gmpg.org/xfn/11" py:attrs="select('@*')" py:with="title = unicode(select('title[1]/text()'))">
 		${select('./*[local-name() != "title"]')}
         <title py:if="title">${title} - djc</title>
 		<title py:if="not title">djc</title>
         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-		<meta name="generator" content="Dan’s blogging engine" />
+		<meta name="generator" content="constance" />
         <link rel="stylesheet" type="text/css" href="${config.REL_BASE}/static/css/common.css" />
-		<link rel="alternate" type="application/atom+xml" title="Atom feed" href="?format=atom" />
 	</head>
 </py:match>
 
 <py:match path="body">
 	<body py:attrs="select('@*')">
-		<div id="leftwrap">
-			<div class="navwidth">
-				<ul class="navigation">
-					<li class="current_page_item"><a href="${config.REL_BASE}/">djc</a></li>
-				</ul>
-			</div>
-			<div id="sidebar"><ul>
-				<li id="feeds">
-					<h2>Feeds</h2>
-					<ul id="feed">
-						<li><a rel="alternate" type="application/atom+xml" href="${config.REL_BASE}/?format=atom" title="Atom feed for posts">Atom posts</a></li>
-					</ul>
-				</li>
-				<li id="archives">
-					<h2>Archives</h2>
-					XXX TODO
-				</li>
-				<li class="linkcat">
-					<h2>Egomania</h2>
-					<ul>
-						<li><a href="http://www.librarything.com/profile/danc86" rel="me" title="My library">Books</a></li>
-						<li><a href="http://www.imdb.com/mymovies/list?l=1447320&amp;s=reverse_uservote" rel="me" title="My IMDB voting history">Movies</a></li>
-						<li><a href="http://www.last.fm/user/danc86" rel="me" title="My last.fm profile">Music</a></li>
-						<li><a href="http://flickr.com/photos/danc86" rel="me" title="My Flickr account">Photos</a></li>
-					</ul>
-				</li>
-				<li class="linkcat">
-					<h2>Randoms</h2>
-					<ul>
-						<li><a href="http://www.houseofzeus.com/notblog/" rel="friend met">flash</a></li>
-						<li><a href="http://protiotype.net/" rel="friend met">io</a></li>
-						<li><a href="http://www.sjkingston.com/" rel="friend met crush">Kingo</a></li>
-						<li><a href="http://mondodev.net/" rel="friend met">mondo</a></li>
-						<li><a href="http://myspace.com/nath520" rel="friend met" title="Fellow SPLC loser">Nath</a></li>
-						<li><a href="http://myspace.com/reesb" rel="friend met">ReesB</a></li>
-						<li><a href="http://www.train-meditations.com/" rel="friend met">Shaun</a></li>
-						<li><a href="http://koffein.net/" rel="friend met crush">stl</a></li>
-						<li><a href="http://myspace.com/heartfelt4ever" rel="friend met">Zeke</a></li>
-					</ul>
-				</li>
-				<li class="linkcat">
-					<h2>Recommended reading</h2>
-					<ul>
-						<li><a href="http://www.aaronsw.com/weblog/">Aaron Swartz</a></li>
-						<li><a href="http://aussielicious.blogspot.com/">aussielicious</a></li>
-						<li><a href="http://www.boingboing.net/">Boing Boing</a></li>
-						<li><a href="http://www.qwantz.com/">Dinosaur Comics</a></li>
-						<li><a href="http://untoward.livejournal.com/">Joey Comeau</a></li>
-						<li><a href="http://reversecowgirlblog.blogspot.com/">Reverse Cowgirl</a></li>
-						<li><a href="http://xkcd.com/">xkcd</a></li>
-					</ul>
-				</li>
-			</ul></div>
-		</div>
-		<div id="content">
-			${select('./*')}
-		</div>
-		<div id="footer">
-			<p>Design derived from <a href="http://www.wpzone.net/">Elite by Stephen Reinhardt</a></p>
-		</div>
+        <div id="contentwrapper">
+            <div id="content">
+                ${select('./*')}
+            </div>
+        </div>
+        <div id="links">
+            <div class="linkrow">
+                <h2>Me</h2>
+                <p>
+                    <a href="http://www.librarything.com/profile/danc86" rel="me" title="My library">Books</a> ${inline_sep()}
+                    <a href="http://www.imdb.com/mymovies/list?l=1447320&amp;s=reverse_uservote" rel="me" title="My IMDB voting history">Movies</a> ${inline_sep()}
+                    <a href="http://www.last.fm/user/danc86" rel="me" title="My last.fm profile">Music</a> ${inline_sep()}
+                    <a href="http://flickr.com/photos/danc86" rel="me" title="My Flickr account">Photos</a>
+                </p>
+            </div>
+            <div class="linkrow">
+                <h2>Randoms</h2>
+                <p>
+                    <a href="http://www.houseofzeus.com/notblog/" rel="friend met">flash</a> ${inline_sep()}
+                    <a href="http://protiotype.net/" rel="friend met">io</a> ${inline_sep()}
+                    <a href="http://www.sjkingston.com/" rel="friend met crush">Kingo</a> ${inline_sep()}
+                    <a href="http://mondodev.net/" rel="friend met">mondo</a> ${inline_sep()}
+                    <a href="http://myspace.com/nath520" rel="friend met" title="Fellow SPLC loser">Nath</a> ${inline_sep()}
+                    <a href="http://myspace.com/reesb" rel="friend met">ReesB</a> ${inline_sep()}
+                    <a href="http://www.train-meditations.com/" rel="friend met">Shaun</a> ${inline_sep()}
+                    <a href="http://koffein.net/" rel="friend met crush">stl</a> ${inline_sep()}
+                    <a href="http://myspace.com/heartfelt4ever" rel="friend met">Zeke</a>
+                </p>
+            </div>
+            <div class="linkrow">
+                <h2>Recommended reading</h2>
+                <p>
+                    <a href="http://www.aaronsw.com/weblog/">Aaron Swartz</a> ${inline_sep()}
+                    <a href="http://www.boingboing.net/">Boing Boing</a> ${inline_sep()}
+                    <a href="http://www.qwantz.com/">Dinosaur Comics</a> ${inline_sep()}
+                    <a href="http://untoward.livejournal.com/">Joey Comeau</a> ${inline_sep()}
+                    <a href="http://reversecowgirlblog.blogspot.com/">Reverse Cowgirl</a> ${inline_sep()}
+                    <a href="http://xkcd.com/">xkcd</a>
+                </p>
+            </div>
+        </div>
 	</body>
 </py:match>
 
diff --git a/templates/_entry.xml b/templates/_entry.xml
@@ -5,75 +5,78 @@
 	 py:def="show_entry(entry, show_comments=True)">
 
 <?python
-import blog
+import blog, config
 from viewutils import mini_markdown, tag_list, category_list
 ?>
 
 <div class="entry" py:if="isinstance(entry, blog.Entry)">
 
-	<h3 class="entrytitle" id="post-${entry.id}"><a href="${config.REL_BASE}/${entry.id}" rel="bookmark">${mini_markdown(entry.title)}</a></h3>
-
-	<div class="entrymeta">
-		Posted ${entry.publication_date.strftime(str('%-1d %B %Y at %H:%M'))}
-		<py:if test="not show_comments and entry.has_comments()">
-			·
-			<a href="${config.REL_BASE}/${entry.id}#comments">
-				Comments
-				<py:if test="len(entry.comments()) > 0">(${len(entry.comments())})</py:if>
-			</a>
-		</py:if>
+	<h3 class="entrytitle" id="post-${entry.id}">${mini_markdown(entry.title)}</h3>
+
+	<div class="entrydate">
+		${entry.publication_date.strftime(str('%-1d %b %Y'))}
+	    <a href="${config.REL_BASE}/${entry.id}" rel="bookmark" class="permalink" title="permalink">#</a>
 	</div>
+
+    <div py:if="entry.tags" class="entrytags">
+        tagged: ${tag_list(entry.tags)}
+    </div>
   
 	<div class="entrybody">
 		${entry.body}
-		<p py:if="entry.tags">
-			<img src="${config.REL_BASE}/static/images/tag_blue.png" alt="Tags:" />
-			${tag_list(entry.tags)}
-		</p>
-	</div>
+    </div>
+
+    <div class="entrycommentslink" py:if="not show_comments and entry.has_comments()">
+        <a href="${config.REL_BASE}/${entry.id}#comments" py:choose="len(entry.comments())">
+            <py:when test="0">no comments »</py:when>
+            <py:when test="1">1 comment »</py:when>
+            <py:otherwise>${len(entry.comments())} comments »</py:otherwise>
+        </a>
+    </div>
+
+    <div class="commentblock" py:if="show_comments and entry.has_comments()"
+         py:with="comments = sorted(entry.comments(), key=lambda c: c.date)">
+        <h4 class="comments" py:choose="len(comments)">
+            <py:when test="0">No comments</py:when>
+            <py:when test="1">1 comment</py:when>
+            <py:otherwise>${len(entry.comments())} comments</py:otherwise>
+        </h4>
+        <div py:for="n, comment in enumerate(comments)" id="comment-${comment.id}">
+            <py:if test="n > 0">${block_sep()}</py:if>
+            ${comment.body}
+            <p class="commentmeta">
+                ― <a py:strip="not comment.author_url" href="${comment.author_url}">${comment.author_name()}</a>
+                <span class="commentdatetime">${comment.date.strftime(str('%-1d %b %Y %H:%M'))}</span>
+                <a href="#comment-${comment.id}" class="permalink" title="permalink">#</a>
+            </p>
+        </div>
+    </div>
 
 </div>
 
 <span py:def="stars(rating)" py:strip="True">
-	<img src="${config.REL_BASE}/static/images/star.png" alt="[star]" py:for="_ in range(int(rating))" />
-	<img src="${config.REL_BASE}/static/images/halfstar.png" alt="[half-star]" py:if="rating > int(rating)" />
+<img src="${config.REL_BASE}/static/images/star.png" alt="[star]" py:for="_ in range(int(rating))" /><img src="${config.REL_BASE}/static/images/star-half.png" alt="[half-star]" py:if="rating > int(rating)" /><img src="${config.REL_BASE}/static/images/star-off.png" alt="" py:for="_ in range(int(5 - rating))" />
 </span>
 
 <div class="entry readinglog" py:if="isinstance(entry, blog.ReadingLogEntry)">
-	<h3 class="entrytitle" id="post-${entry.id}">
-		<a py:strip="not entry.url" href="${entry.url}">${mini_markdown(entry.title)}</a>
-		<py:if test="entry.author">
-			by ${entry.author}
-		</py:if>
-	</h3>
-	<div class="entrymeta">
-		Posted ${entry.publication_date.strftime(str('%-1d %B %Y'))}
-		<py:if test="entry.rating">
-			· ${stars(entry.rating)}
-		</py:if>
-	</div>
-	<div class="entrybody"></div>
-</div>
 
-<div id="commentblock"
-	 py:if="show_comments and entry.has_comments()"
-	 py:with="comments = sorted(entry.comments(), key=lambda c: c.date)">
-	<h2 id="comments">${len(comments) == 1 and '1 comment' or '%d comments' % len(comments)} so far</h2>
-    <ol class="commentlist" id="commentlist">
-		<li py:for="n, comment in enumerate(comments)"
-			class="${(n % 2) and 'alt' or 'standard'}"
-			id="comment-${comment.id}">
-			<div class="commentname">
-				<a py:strip="not comment.author_url" href="${comment.author_url}">${comment.author_name()}</a>
-				on <a href="#comment-${comment.id}">${comment.date.strftime(str('%-1d %B %Y'))}</a>
-			</div>
-			<div class="commenttext">
-				<div class="commentp">
-					${comment.body}
-				</div>
-			</div>
-		</li>
-	</ol>
+    <img py:if="entry.isbn" class="cover" src="http://covers.librarything.com/devkey/${config.LIBRARYTHING_DEVKEY}/small/isbn/${entry.isbn}" />
+
+    <h3 class="entrytitle" id="post-${entry.id}">
+        <a py:strip="not entry.url" href="${entry.url}">${mini_markdown(entry.title)}</a>
+        <py:if test="entry.author">
+            <span class="author">by ${entry.author}</span>
+        </py:if>
+    </h3>
+
+    <div class="entrydate">
+        ${entry.publication_date.strftime(str('%-1d %b %Y'))}
+    </div>
+
+    <div py:if="entry.rating" class="rating">
+        ${stars(entry.rating)}
+    </div>
+
 </div>
 
 </div>
diff --git a/templates/multiple.xml b/templates/multiple.xml
@@ -8,21 +8,17 @@
 
 <head>
 	<title py:if="title">${title}</title>
+    <link rel="alternate" type="application/atom+xml" title="Atom feed" href="?format=atom" />
+    <link py:if="bool(offset)" rel="prev" href="?offset=${max(0, offset - 20)}" />
+    <link py:if="len(sorted_entries) > offset + config.ENTRIES_PER_PAGE" rel="next" href="?offset=${offset + 20}" />
 </head>
 <body>
 
 <h2 class="archives" py:if="title">Archive for the ${title}</h2>
 
-<py:with vars="show_prev = bool(offset); show_next = len(sorted_entries) > offset + 20">
-	<py:for each="entry in sorted_entries">
-		${show_entry(entry, show_comments=False)}
-	</py:for>
-	<p py:if="show_prev or show_next">
-		<a py:if="show_prev" href="?offset=${max(0, offset - 20)}">« previous page</a>
-		<py:if test="show_prev and show_next">—</py:if>
-		<a py:if="show_next" href="?offset=${offset + 20}">next page »</a>
-	</p>
-</py:with>
+<py:for each="entry in sorted_entries[offset:offset + config.ENTRIES_PER_PAGE]">
+    ${show_entry(entry, show_comments=False)}
+</py:for>
 
 </body>
 </html>
diff --git a/templates/multiple_atom.xml b/templates/multiple_atom.xml
@@ -4,16 +4,16 @@
 	  xmlns:xi="http://www.w3.org/2001/XInclude">
 
 <?python
-import config
+import config, blog
 from viewutils import tag_list
 ATOM_TIME_FORMAT = str('%Y-%m-%dT%H:%M:%S+10:00')
 ?>
 
 <id>${config.ABS_BASE}/?format=atom</id>
-<title type="text">${config.BLOG_NAME}</title>
-<link rel="self" type="application/atom+xml" href="${config.ABS_BASE}/?format=atom" />
-<link rel="alternate" href="${config.ABS_BASE}/" />
-<generator>Dan’s blogging engine</generator>
+<title type="text">${config.BLOG_NAME}<py:if test="title"> (${title})</py:if></title>
+<link rel="self" type="application/atom+xml" href="${url}?format=atom" />
+<link rel="alternate" href="${url}" />
+<generator>constance</generator>
 <updated>${feed_updated.strftime(ATOM_TIME_FORMAT)}</updated>
 
 <entry py:for="entry in sorted_entries">
@@ -24,20 +24,16 @@ ATOM_TIME_FORMAT = str('%Y-%m-%dT%H:%M:%S+10:00')
 		<name>${config.BLOG_AUTHOR}</name>
 		<email py:if="config.BLOG_EMAIL">${config.BLOG_EMAIL}</email>
 	</author>
-	<category py:for="category in entry.categories" scheme="${config.ABS_BASE}/+categories/" term="${category}" />
 	<category py:for="tag in entry.tags" scheme="${config.ABS_BASE}/+tags/" term="${tag}" />
-	<py:if test="'Reading' not in entry.categories">
+	<py:if test="isinstance(entry, blog.Entry)">
 		<link rel="alternate" href="${config.ABS_BASE}/${entry.id}" />
 		<title type="text">${entry.title}</title>
 		<content type="xhtml" xml:base="${config.ABS_BASE}/${entry.id}"><xhtml:div>
+			<p py:if="entry.tags">Tagged: ${tag_list(entry.tags)}</p>
 			${entry.body}
-			<p py:if="entry.tags">
-				<img src="${config.ABS_BASE}/static/images/tag_blue.png" alt="Tags:" />
-				${tag_list(entry.tags)}
-			</p>
 		</xhtml:div></content>
 	</py:if>
-	<py:if test="'Reading' in entry.categories">
+	<py:if test="isinstance(entry, blog.ReadingLogEntry)">
 		<title type="text">${entry.title} by ${entry.author}</title>
 		<summary type="text">${entry.rating} stars</summary>
 		<content type="xhtml"><xhtml:div>