Friday, August 29th, 2008 (updated 2 Mar ’10)

adam@engaging.net | Brighton, England

How Subtraction.com was converted to EE

 
B

eing a blog, Khoi Vinh’s renowned Subtraction.com isn’t as complex a web site as one for an entire organization. But being a standard-bearer of design excellence, everything has to be just so. As Khoi switched to EllisLab’s ExpressionEngine web publishing system, some of the details he wanted fell through the cracks of the software’s out-the-box functionality. This article explains how those details were implemented and how the site was architected in EE.

  • Sector
  • Service

For the conversion of the renowned Subtraction.com web site to the renowned ExpressionEngine web publishing system, the key architectural insight was that all except one of the major screens are either single-entry or multi-entry. The various multi-entry screens are therefore powered by just one multi-entry template and the single-entry screens by just one single-entry template, reducing the number of templates required, making the site easier to maintain.

The Index Template

In turn, both the single- and the various multi-entry screens spark from a single template, /site/index:

{embed=“site/header”}

{if segment_1==”“ }
{!— Display multi-entry as homepage —}
{embed=“site/multi-entry” pagetype=“homepage”}

{if:elseif segment_1 != “” && segment_1 != “archives” && segment_1 != “categories” && segment_1 != “about” && segment_4==”“
}
{!— Display multi-entry as monthly —}
{if segment_1 > 2000 && segment_2 > 1 && segment_2 < 13}
{embed=“site/multi-entry” pagetype=“monthly”}
{/if}

{if:elseif segment_1 == “categories”}
{!— Display multi-entry as category —}
{embed=“site/multi-entry” pagetype=“category”}

{if:elseif segment_4}
{!— Display single-entry —}
{embed=“site/single-entry”}
{/if}

{embed=“site/footer”}

Here the first and last lines show that the header and footer are shunted out of the way from the get-go; any header variations among the screens are handled within the /site/header template. Into these site-wide standards is shoveled as much as possible in order to reduce the main templates.

The heart of /site/index is a conditional to test various URL structures. If for instance our URL has four segments then we’re on a single-entry page, as the site uses the blog-standard /year/month/date/dirified-title URL format for entries and no other screens have so many URL segments.

The Single-Entry Template

EE is not, despite its blogging software pedigree, designed for blog-standard URLs. Instead, its URLs use a /template-group/template/dirified-title format. Khoi was adamant however that his blog-standard URLs must stay. So this is handled in /site/single-entry:

{exp:weblog:entries weblog=“subtraction|lens|elsewhere” disable=“category_fields|pagination|trackbacks” year=”{segment_1}” month=”{segment_2}”  url_title=”{segment_4}” dynamic=“off” limit=“1” status=“not closed”}
{if member_group==“1” || status==“open”}

<div id=“posts”>

<div class=“day”>

<p class=“date”>{entry_date format=’%D %d %M’}<br /> {entry_date format=’%Y’}</p> <h2{if status==“Hold”} style=“color: red”{/if}>{title}</h2> <div class=“post-info”> <span class=“info-label”>Posted</span> <span class=“info-data”>{entry_date format=’%h:%i %a’}</span> </div> <div class=“post-info”> <span class=“info-label”>Author</span> <span class=“info-data”><a href=”{url}”>{author}</a></span> </div> {if weblog_short_name == “subtraction”} <div class=“post-info”> <span class=“info-label”>Categories</span> <span class=“info-data”>{categories backspace=“1” style=“linear”}<a href=”{path=/}”>{category_name}</a>, {/categories}</span> </div> <div class=“body-lead”> <div class=“info-label”>Body</div> <div class=“first”> {body} </div> {extended} </div><!— /.body-lead —> <div id=“terminator”>+</div> <div class=“clear”></div> {/if} {if weblog_short_name == “elsewhere”} <div class=“post-info”> <span class=“info-label”>Rating</span> <span class=“info-data”> <div class=“stars”> <a href=”“ class=“stars-{elsewhere-rating}”><img src=”{site_url}assets/1-star.gif” alt=“Stars” width=“14” height=“14” /></a> </div> <div class=“clear”></div> </span> </div> <div class=“body-lead”> <div class=“info-label”>Body</div> <div class=“first”> {elsewhere-body} </div> </div><!— /.body-lead —> <div id=“terminator”>+</div> <div class=“clear”></div> <div class=“post-info post-info-elsewhere”> <span class=“info-label”>Link</span> <span class=“info-data”> <div class=“elsewhere-link”><a href=”{elsewhere-url}”>Jump to This Link</a></div> </span> </div> {/if} {if weblog_short_name == “lens”} <div class=“post-info”> <span class=“info-label”>Categories</span> <span class=“info-data”>  {categories backspace=“2” style=“linear”}<a href=”{path=weblog/index}”>{category_name}</a>, {/categories} </span> </div> <div class=“body-lead {lens-orientation}”> {lens-image} {if lens-caption != “” } {lens-caption} {/if} {if lens-caption-extended != “” } {lens-caption-extended} {/if} </div><!— /.body-lead —> <div id=“terminator”>+</div> <div class=“clear”></div> {/if}

</div><!— /.day —>

<?php
$still_allow_comments=“no”;
$post_time=strval(“{entry_date format=’%U’}”);
$current_time=”{current_time}”;
$comments_allowed_from = $current_time – 5184000;
if ( $post_time >= $comments_allowed_from )
{ $still_allow_comments=“yes”;
}
?>

{if allow_comments && comment_total 0} {if:elseif allow_comments && comment_total >= 1} <a name="remarks"></a> <div id="remarks"> <h3> <span class="notation">Remarks</span> <?php if ( $still_allow_comments “yes” ) : ?> {comment_total} total remarks on this post. <a href=”#add-form”>Add your own remarks below.</a> <?php else: ?> {comment_total} total remarks were added before the post was closed. <?php endif; ?> </h3>
{/if}

{embed=“site/display-comments” thisentry_id=”{entry_id}”}

{if member_group==“1” || status==“open”}
</div><!— /.remarks —>
{/if}

<?php
if ( $still_allow_comments == “yes” ) :
?>

<div class=“add-remarks”>

{exp:comment:form preview=“site/comment_preview” url_title=”{segment_4}”} <h3>Add Remarks<a name=“add-form”></a></h3> <div class=“guidelines”>Please be nice.</div> {if logged_in} <div class=“add-lead”> <label for=“name”>Name</label> <div><input type=“text” size=“3” id=“name” name=“name” value=”{name}” /><span>Your name is required.</span></div> </div><!— /.add-lead —> <div class=“clear”></div> <div class=“add-lead”> <label for=“email”>Email</label> <div><input type=“text” size=“3” id=“email” name=“email” value=”{email}” /><span>An email address is required.</span></div> </div><!— /.add-lead —> <div class=“clear”></div> <div class=“add-lead”> <label for=“url”>Web Site</label> <input type=“text” size=“3” id=“url” name=“url” value=”{url}” /> </div><!— /.add-lead —> <div class=“clear”></div> {/if} {if logged_out} <div class=“add-lead”> <label for=“remember”>Your Info</label> <input type=“checkbox” name=“save_info” value=“yes” {save_info} /> <span>Save my personal information for next time</span> </div><!— /.add-lead —> <div class=“clear”></div> {/if} <div class=“add-lead”> <label for=“remember”>Follow-up</label> <input type=“checkbox” name=“notify_me” value=“yes” {notify_me} /> <span>Notify me of further remarks on this post</span> <div class=“clear”></div> </div><!— /.add-lead —> <div class=“add-lead”> <label class=“your-remarks”>Your Remarks</label> <textarea name=“comment” rows=“10” cols=“40” class=“your-remarks”>{comment}</textarea> </div><!— /.add-lead —> <div class=“add-last-lead”> <label>Submit</label> <span><input type=“submit” name=“preview” value=“Preview” /></span> <span><input type=“submit” name=“submit” value=“Submit” /></span> </div><!— /.add-lead —> {/exp:comment:form}

</div><!— /.add-remarks —>

<?php endif; ?>

</div><!— /#posts —>

{embed=“site/sidebar” pagetype=“article” articletype=”{lens-orientation}” }

{/if}
{/exp:weblog:entries}

Into the template’s sole weblog:entries tag (always a good goal for performance reasons) a few extra parameters have been added:

year="&#123;segment_1&#125;" month="&#123;segment_2&#125;" url_title="&#123;segment_4&#125;" dynamic="off" limit="1"

The tag’s “dynamic” parameter switches off EE’s default method of retrieving the entry from the URL. Instead, the year is tested against the contents of segment #1, the month against segment #2 and the dirified URL against segment #4. (The date isn’t tested against segment #3 because this seems to filter out entries posted between 12:00am and 1:00am.)

The “weblog” parameter contains all three weblogs — subtraction, lens and elsewhere. Initially the template handles entries similarly regardless of their weblog, displaying the title, date and author. Then entries fork depending on their weblog.

After the “day” div are a few lines of PHP beginning with

still_allow_comments="no";

Khoi wants each post’s comments to close after 60 days. While EE does have a comment expiration field in the Publish/Edit Form, it’s a pity to set each post’s expiration date manually when a single 60-day rule covers them all. So the PHP here tests whether the entry is less than 60 days old, and if it is, the variable $still_allow_comments is set to “yes”, displaying the comment area’s header as an invitation to comment rather than a notice that comments are closed, followed later by the comment form.

Near the end of the template is the call to the sidebar. This call could have been made from the footer except for the sidebar needing to know if our page is a Lens entry with a landscape-shaped image, and if so to not display certain sidebar modules since they conflict with the image. (The sidebar template could also find out what it needs by running the weblog:entries tag itself.)

The comments are displayed in a sub-template /site/display-comments because we need them to be outside of the weblog:entries tag in order to display Khoi’s comments differently (though in retrospect that could probably be handled with a PHP conditional):

&#123;exp:comment:entries weblog="subtraction|lens|elsewhere" dynamic="off" entry_id="&#123;embed:thisentry_id&#125;"&#125;
&lt;div class="remarks-lead&#123;if author_id=="1"&#125; owner&#123;/if&#125;"&gt;
 &lt;div class="remarks-date"&gt;
  &lt;b&gt;&#123;comment_date format='%D %d %M %Y'&#125;&lt;/b&gt; at &#123;comment_date format='%h&#58;%i %a'&#125;&lt;br /&gt;
  &#123;url_as_author&#125;
 &lt;/div&gt;
 &#123;comment&#125;
 &lt;div class="clear"&gt;&lt;/div&gt;
&lt;/div&gt;&lt;!-- /.remarks-lead --&gt;
&#123;/exp:comment:entries&#125;

The Sidebar Template

/site/sidebar:

<div id=“sidebar”>

{!— QUICK ACCESS: Display on Homepage, Monthly, Category, Article —} {if “{embed:pagetype}” "homepage" || "{embed:pagetype}" “monthly” || “{embed:pagetype}” "category" || "{embed:pagetype}" “article” || “{embed:pagetype}” == “about” } <div class=“sidebar-module”> <h3>Quick Access</h3> {exp:stats weblog=“subtraction|elsewhere|lens”} <?php $total_entries = number_format(“{total_entries}”); ?> {/exp:stats} <p class=“first”><a href=“http://www.subtraction.com/archives/”><?php echo $total_entries; ?></a> posts since July 2000.</p> <div class=“full-archives”> <label>Dates</label> <div class=“pulldown”> <select name=“date-archive” onchange=“jumpMenu(‘parent’,this,0)”> <option value=”“ selected=“selected”>Select…</option> {exp:weblog:month_links limit=“12”} <option value=”{path=/}”>{month} {year}</option> {/exp:weblog:month_links} <option value=“http://www.subtraction.com/archives/”>All Older Posts</option> </select> </div><!— /.pulldown —> </div><!— /.full-archives —> <div class=“full-archives”> <label>Categories</label> <div class=“pulldown”> <select name=“date-archive” onchange=“jumpMenu(‘parent’,this,0)”> <option value=”“ selected=“selected”>Select…</option> {exp:weblog:categories weblog=“subtraction” category_group=“1” style=“linear” orderby=“title” disable=“category_fields” } <option value=”{path=/}”>{category_name}</option> {/exp:weblog:categories} </select> </div><!— /.pulldown —> </div><!— /.full-archives —> </div><!— /.sidebar-module —> {/if} {!— RECENT POSTS: Display on Monthly, Category, Article providing not Landscape —} {if “{embed:pagetype}” "monthly" || "{embed:pagetype}" “category” || “{embed:pagetype}” == “article” && “{embed:articletype}” != “landscape” } <div class=“sidebar-module”> <h3>Recent Posts</h3> {exp:weblog:entries orderby=“date” sort=“desc” limit=“5” weblog=“subtraction” dynamic=“off” disable=“pagination|categories|member_data|trackbacks”} <p class=“date”>{entry_date format=’%d %M %Y’}</p> <h4><a href=”{path={entry_date format=”>{title}</a></h4> {summary}{lens-caption} {/exp:weblog:entries} </div><!— /.sidebar-module —> {/if} {!— ABOUT ME: Display on Homepage —} {if “{embed:pagetype}” == “homepage”} {!— Display on homepage —} <div class=“sidebar-module”> <h3>About Me</h3> <p class=“first”>I work as the Design Director at <a href=“http://www.nytimes.com”>NYTimes.com</a>, and I have a dog named <a href=“http://www.misterpresident.org/”>Mister President</a>. <a href=“http://www.subtraction.com/about/”>Read more</a>.</p> </div><!— /.sidebar-module —> {/if} {!— PLEASE NOTE: Display on Homepage —} {if “{embed:pagetype}” == “homepage”} <div class=“sidebar-module”> <h3>Please Note</h3> <p class=“first”>Some things that may interest you.</p> <a href=“http://www.abriefmessage.com/”><img src=“http://www.subtraction.com/images/tout_abm.gif” alt=“A Brief Message” width=“190” height=“170” /></a> <a href=“http://www.misterpresident.org”><img src=“http://www.subtraction.com/images/tout_mr_president.gif” alt=“MisterPresident.org” width=“190” height=“50” /></a> <a href=“http://www.subtraction.com/archives/2007/0827_a_subway_sys.php”> <img src=“http://www.subtraction.com/images/tout_subway_maps.gif” alt=“Subway Maps for iPhone” width=“190” height=“50” /> </a> </div><!— /.sidebar-module —> {/if} {!— GET YOUR XML ON: Display on Homepage —} {if “{embed:pagetype}” == “homepage”} {!— Display on homepage —} <div class=“sidebar-module”> <h3>Get Your XML On</h3> <ul class=“xml”> <li><a href=“http://feeds.feedburner.com/subtraction” class=“atom”>Subscribe to the Feed</a></li> </ul> </div><!— /.sidebar-module —> {/if} {!— COLOPHON: Display on Homepage —} {if “{embed:pagetype}” == “homepage”} {!— Display on homepage —} <div class=“sidebar-module”> <h3>Colophon</h3> <ul class=“colophon”> <li><a href=“http://www.apple.com” class=“mac”>Made with a Macintosh</a></li> <li><a href=“http://www.expressionengine.com” class=“ee”>Published with Expression Engine</a></li> <li><a href=“http://creativecommons.org/licenses/by-nc-sa/1.0” class=“cc”>Creative Commons License</a></li> </ul> </div><!— /.sidebar-module —> {/if} {!— RECENT REMARKS: Display on Homepage —} {if “{embed:pagetype}” == “homepage”} <div class=“sidebar-module”> <h3>Recent Remarks</h3> <?php $count=”“; ?> {exp:weblog:entries sort=“desc” orderby=“date” limit=“10” dynamic=“off”} {if comment_total > 0} <?php $comments = “{comment_total}”; $commentsShow = $comments -1; if ($count < 6 ) : $count++; ?> {embed=“embeds/recent-remarker” thisentry_urltitle=”{url_title}” commentsShow=”<?php echo $commentsShow?>” datepath=”{entry_date format=”%Y/%m/%d/”}”} <?php endif; ?> {/if} {/exp:weblog:entries} </div><!— /.sidebar-module —> {/if}

</div><!— /sidebar —>

The sidebar is comprised of a series of modules — Quick Access, Recent Posts, About Me, Please Note, Get Your XML On, Colophon and Recent Remarks — some static, some dynamic, each displayed or not depending on the value of the pagetype variable, which is always passed to the sidebar template by its parent.

For recent remarks, a template is embedded to display the comment:entries tag, with data passed to it from the weblog:entries tag. This is required because the comment:entries tag is missing some vital variables, such as entry_date and url_title. The embedded template is /embeds/recent-remarker:

&#123;exp:comment:entries url_title="&#123;embed:thisentry_urltitle&#125;" 
  disable="categories|custom_fields|member_data|pagination|trackbacks" limit="1"&#125;
&lt;p class="date"&gt;&#123;comment_date format="%d %M %Y"&#125;&lt;/p&gt;
&lt;h4&gt;&#123;name&#125; &#123;if embed:commentsShow &gt; 0&#125;(and &#123;embed:commentsShow&#125; others) &#123;/if&#125;on &lt;a href="&#123;path=/&#125;&#123;embed:datepath&#125;&#123;url_title&#125;"&gt;&#123;title&#125;&lt;/a&gt;&lt;/h4&gt;
&#123;/exp:comment:entries&#125;

The Multi-Entry Template

That’s it for a single entry, now for the multi-purpose /site/multi-entry template:

<div class=“clear”></div>

{if embed:pagetype "homepage"} <div id="cover"> </div> {if:elseif embed:pagetype “monthly” }
{embed=“embeds/top-monthly”}
{if:elseif embed:pagetype == “category”}
{embed=“embeds/top-categories”}
{/if}

<div id=“posts”>

{if segment_1 == “”} {embed=“site/multi-entry-inner” entrieslimit=“60”} {if:else} {embed=“site/multi-entry-inner” entrieslimit=“999”} {/if} {!— if “{embed:pagetype}” != “category”} {embed=“embeds/posts-more”} {/if —}

</div><!— /#posts —>

{embed=“site/sidebar” pagetype=”{embed:pagetype}” articletype=”“}

The multi-entry template contains only three parts: its top, the posts, and the call to the sidebar. The top varies depending on whether the template has been called to display the homepage, a monthly or a category page.

The Multi-Entry Inner Template

If we’re on the homepage, the /site/multi-entry-inner template is called with a limit of 60 entries:

<?php 
$daylimit=”“;
$daycount=”“;
?>

{exp:weblog:entries weblog=“subtraction|elsewhere|lens” orderby=“date” sort=“desc” limit=”{embed:entrieslimit}” disable=“member_data|trackbacks” relaxed_categories=“yes”}
{date_heading display=“daily”}
<?php $daylimit++; if ($daylimit==“1”) { $daylimit=“2”; } ?>
{/date_heading}
{/exp:weblog:entries}

<?php if (“{embed:entrieslimit}” != “60”) { $daylimit=“999”; } ?>

{exp:weblog:entries weblog=“subtraction|elsewhere|lens” orderby=“date” sort=“desc” limit=”{embed:entrieslimit}” disable=“member_data|trackbacks” relaxed_categories=“yes”}

{date_heading display=“daily”}
<?php $daycount++; if ($daycount < $daylimit) : ?>
<div class=“day”> <p class=“date”> {entry_date format=’%D %d %M’}<br /> {entry_date format=’%Y’} </p> <?php endif; ?> {/date_heading}

<?php if ($daycount < $daylimit) : ?> <?php $still_allow_comments=“no”; $post_time=strval(“{entry_date format=’%U’}”); $current_time=”{current_time}”; $comments_allowed_from = $current_time – 5184000; if ( $post_time >= $comments_allowed_from ) { $still_allow_comments=“yes”; } ?> {if weblog_short_name "elsewhere"} <h2 id="{entry_id}" class="elsewhere"><a href="{elsewhere-url}">{title}</a></h2> <div class="post-lead"> <div class="time-remarks"> <div class="stars"> <a href="{path={entry_date format=" class="stars-{elsewhere-rating}"> <img src="{site_url}assets/1-star.gif" alt="Stars" width="14" height="14" /> </a> </div><!-- /.stars --> </div><!-- /.time-remarks --> {if elsewhere-body != "" } {elsewhere-body} {if:elseif elsewhere-body “”} <p> </p> {/if} </div><!— /.post-lead —> <div class=“clear”></div> {/if} {if weblog_short_name "lens"} <h2 id="{entry_id}"> <a href="{path={entry_date format=" format="%m" format="%d">{title}</a> </h2> <div class="post-lead lens"> <div class="time-remarks"> {entry_date format="%g:%i %A"}<br /> {if allow_comments && comment_total 0} <?php if ( $still_allow_comments == “yes” ) : ?> <a href=”{path={entry_date format=”>Add Remarks</a> <?php endif; ?> {if:elseif allow_comments && comment_total >= 1} <a href=”{path={entry_date format=”>Remarks ({comment_total})</a> {/if} </div><!— /.time-remarks —> <a href=”{path={entry_date format=”>{lens-preview}</a> {if lens-caption != “” } {lens-caption} {/if} <div class=“full”> <a href=”{path={entry_date format=”>View Full Image</a> </div> {if allow_comments && comment_total 0} <?php if ( $still_allow_comments “yes” ) : ?> <div class=“add”><a href=”{path={entry_date format=”>Add First Remarks</a></div> <?php endif; ?> {if:elseif allow_comments && comment_total >= 1} <?php if ( $still_allow_comments == “yes” ) : ?> <div class=“add”><a href=”{path={entry_date format=”>Add Remarks ({comment_total} so far)</a></div> <?php endif; ?> {/if} </div><!— /.post-lead —> <div class=“clear”></div> {/if} {if weblog_short_name "subtraction"} <h2 id="{entry_id}"><a href="{path={entry_date format=" format="%m" format="%d">{title}</a></h2> <div class="post-lead"> <div class="time-remarks"> {entry_date format="%g:%i %A"}<br /> {if allow_comments && comment_total 0} <?php if ( $still_allow_comments "yes" ) : ?> <a href="{path={entry_date format=">Add Remarks</a> <?php endif; ?> {if:elseif allow_comments && comment_total >= 1} <a href="{path={entry_date format=">Remarks ({comment_total})</a> {/if} </div><!-- /.time-remarks --> {if "{embed:pagetype}" != "category"} {body} {if extended != "" } <div class="continue"> <a href="{path={entry_date format=">Continue Reading</a> </div> {/if} {if:else} {summary} {/if} <?php if ( $still_allow_comments “yes” ) : ?> {if allow_comments && comment_total == 0} <div class=“add”> <a href=”{path={entry_date format=”>Add First Remarks</a> </div> {if:elseif allow_comments && comment_total >= 1} <div class=“add”> <a href=”{path={entry_date format=”>Add Remarks ({comment_total} so far)</a> </div> {/if} <?php endif; ?> </div><!— /.post-lead —> <div class=“clear”></div> {/if}

{date_footer display=“daily”}</div><!— /.day —>{/date_footer}

<?php endif; ?>

{/exp:weblog:entries}

Why is an inner template necessary? A choices of two values is required for the weblog:entries “limit” parameter because the homepage displays only the newest 60 entries whereas the other multi-entry pages — months and categories — display an unlimited number of entries. The problem is that a variable generated within a template doesn’t work as a weblog:entries tag’s limit parameter value due to the template parsing order. But variables passed from a parent template can be used there.

The first thing to notice is that the weblog:entries tag is called twice. While the homepage limit is 60 entries, Subtraction.com is organized daily, so if the oldest day contains say 5 entries but 57 have already been displayed, then only three of the final day’s entries will be shown. Khoi wanted the homepage’s final day to appear completely. So in the first call of the weblog:entries tag the number of days covered is counted as $daylimit, then in the second weblog:entries tag $daycount is incremented for each day, the day only displayed if the $daycount is smaller than the $daylimit:

&lt;?php $daycount++; if ($daycount &lt; $daylimit) : ?&gt;
...
&lt;?php endif; ?&gt;

The result is that the final day, whether incomplete or not, is truncated from the homepage. (If we’re not on the homepage, the daylimit is set to 999 because we don’t need to apply it.)

Unlike on the single-entry template, each of the three weblogs is displayed differently enough here to warrant its own fork. But like the single-entry template, PHP code calculates whether comments are still allowed, determining whether the links read “Add Remarks” or just “Remarks”.

The Top Categories Template

Back up to the /site/multi-entry template, if we are not on the homepage but on a categories page then the conditional embeds the template /embeds/top-categories:

&lt;div id="category-header"&gt;
 &lt;?php $catposts=""; ?&gt;
 &#123;exp:weblog:entries weblog="subtraction|elsewhere|lens" orderby="date" sort="desc" limit="999" 
 disable="category_fields|custom_fields|member_data|pagination|trackbacks" 
 relaxed_categories="yes" status="not closed|Hold"&#125;
 &#123;if count=="1"&#125;
 &lt;?php $catposts="&#123;total_results&#125;"; ?&gt;
 &#123;/if&#125;
 &#123;/exp:weblog:entries&#125; 
 &#123;exp:weblog:category_heading weblog="subtraction|lens|elsewhere" relaxed_categories="yes"&#125;
 &lt;h1&gt;&#123;category_name&#125;&lt;/h1&gt;
 &lt;h3&gt;&lt;?php echo $catposts; ?&gt; posts&lt;/h3&gt;
 &#123;if category_description&#125;
 &lt;p&gt;&#123;category_description&#125;&lt;/p&gt;
 &#123;/if&#125;
 &#123;/exp:weblog:category_heading&#125; 
&lt;/div&gt;

Here the weblog:entries tag is run before the weblog:category_heading tag because category_heading lacks a count feature, and Khoi wasn’t doing without that. Using a PHP variable the weblog:entries tag enables us to remember the number of entries as $catposts. Since we’re in a /categories URL, which is what we’ve set as EE’s Reserved Category URL Indicator, the weblog:entries tag does not require a specified category as it’s already active. Then the weblog:category_heading tag displays the category’s name and description, if there is one, together with our $catposts.

The Top Monthly Template

If however we’re on a monthly page then instead of /embeds/top-categories the top is /embeds/top-monthly:

{exp:weblog:entries weblog=“illustrate_me” limit=“1” disable=“member_data|trackbacks”}
<div id=“illustration”> <img src=”{illustrate_me_illustration}” alt=”“ />
</div>
{/exp:weblog:entries}

<?php
$monthcount = “”;
$total_subtraction=”“;
$total_lens=”“;
$total_elsewhere=”“;
?>

{exp:weblog:entries weblog=“subtraction|elsewhere|lens” disable=“member_data|trackbacks” limit=“999” disable=“categories|category_fields|custom_fields|member_data|pagination|trackbacks” status=“not closed|Hold”}
{if count==“1”}
<?php
$monthcount = “{total_results}”;
?>
{/if}
{/exp:weblog:entries}

<?php $row=”“; ?>
{exp:weblog:calendar weblog=“subtraction|lens|elsewhere” leading_zeroes=“yes”}
<div id=“calendar”>
<h1 class=“page-title”>{date format=”%F %Y”}

<span><?php if ($monthcount) { echo $monthcount . “ posts”; } ?></span>
</h1>

<table border=“0” cellspacing=“0” cellpadding=“0”>

<tr>
<th>Sunday</th>
<th>Monday</th>
<th>Tuesday</th>
<th>Wednesday</th>
<th>Thursday</th>
<th>Friday</th>
<th class=“last”>Saturday</th>
</tr>

{calendar_rows} {row_start}<tr>{/row_start} {if entries} <td> <p>{day_number}</p> {entries} <a href=”{path={entry_date format=” title=”{title}”><img src=”{site_url}assets/calendar_markers.gif” alt=”{title}” width=“95” height=“10” /></a> {/entries} </td> {/if} {if not_entries} <td> <p>{day_number}</p> <img src=”{site_url}assets/calendar_markers.gif” alt=”“ width=“95” height=“10” /> </td> {/if} {if blank} <td><p></p></td> {/if} {row_end}</tr>{/row_end} {/calendar_rows}

</table>
</div>
{/exp:weblog:calendar}

{exp:weblog:entries weblog=“illustrate_me” limit=“1” disable=“member_data|trackbacks”}
{exp:replace find=”</p<” replace=”“}{illustrate_me_caption}{/exp:replace} {related_entries id=“illustrate_me_subtraction”}Read the <a href=”{path={entry_date format=”>interview about this illustration</a>.</p>{/related_entries}
{/exp:weblog:entries}

<div class=“clear”></div>

Here too links get an assist from EE’s natural functioning: in this template’s three weblog:entries tags neither the year nor month need be specified because the system already grabs them from the numerical /year/month format of the URL. Yes, parameters could be added explicitly to the weblog:entries tags:

year="&#123;segment_1&#125;" month="&#123;segment_2&#125;" dynamic="off"

But it’s nice to let her sail as she was designed.

The first weblog:entries tag displays the discontinued Illustrate Me image if it exists for that month. Although Khoi does not plan to revive this feature, it’s easier to let EE handle the scheduling of discontinued content types just the same way it handles ongoing ones. So Illustrate Me has a weblog just as do Subtraction, Elsewhere and Lens.

The next weblog:entries instance is similar to the one in the /embeds/top-categories template that counts entries in the category, except this time we’re counting entries in the month. (Note that here all options are disabled while in /embeds/top-categories categories was still enabled.)

Then within Khoi’s sleek-looking calendar we list any Subtraction, Lens or Elsewhere entries, linking as always to their blog-style URLs:

&#123;path=&#123;entry_date format="%Y/%m/%d"&#125;&#125;&#123;url_title&#125;

At the end of the template the Illustrate Me weblog is called again to retrieve and display its caption, which appears after the calendar. (To save server processsing we could have grabbed this data from the earlier call to Illustrate Me, setting it there as a PHP variable to display later down below.)

The Archives Template

Just one major screen remains, the Archives, which differs from all the others so requires its own template:

{if segment_2 == “”}

{embed=“site/header”}

<div id=“column-a”> <h1>Archives<br />by Date</h1>

<?php $x=”“; ?> {exp:yearly_archives weblog=“subtraction|lens|elsewhere” status=“not closed” monthsort=“desc” show_future_entries=“no”} {months} {if num_entries} <?php $x++; if ( $x < 4) : ?> <div class=“archive-unit”> <h2><a href=”{path=/}{year}/{month_num}”><span>{month} {year}</span>{num_entries} posts</a></h2> {embed=“embeds/archives-date” year=”{year}” month=”{month_num}”} <?php else : ?> {if month_num==“12”}<h2 style=“clear: both; text-align: left”>{year}</h2>{/if} <div class=“archive-subunit”> <h2><a href=”{path=/}{year}/{month_num}”><span>{month}<!— {year}—></span><br />{num_entries} posts</a></h2> <?php endif; ?> </div><!— /.archive-unit —> {/if} {/months} {/exp:yearly_archives}

</div><!— /#column-a —>

<div id=“column-b”>

<h1>Archives<br />by Category</h1> {exp:query sql=“SELECT count(exp_category_posts.entry_id) AS post_count, exp_category_posts.cat_id, exp_categories.cat_name, exp_categories.cat_url_title, exp_categories.cat_description FROM exp_categories, exp_category_posts, exp_weblog_titles WHERE exp_category_posts.cat_id = exp_categories.cat_id AND (exp_weblog_titles.weblog_id = ‘1’ || exp_weblog_titles.weblog_id = ‘2’ || exp_weblog_titles.weblog_id = ‘4’) AND exp_weblog_titles.entry_id = exp_category_posts.entry_id GROUP BY exp_categories.cat_name”} <div class=“archive-unit”> <h2><a href=”{path=tags}{cat_url_title}”><span>{cat_name}</span>{post_count} posts</a></h2> {if cat_description}<p>{cat_description}</p>{/if} </div><!— /.archive-unit —> {/exp:query}

</div><!— /#column-b —>

{embed=“site/footer”}

{/if}
{if segment_2 != “”}

<?php
$original_segment2 = “{segment_2}”;
$year = $original_segment2;
$original_segment3 = “{segment_3}”;
$month = substr($original_segment3, 0, 2);
$date = substr($original_segment3, 2, 2);
$urltitle = substr($original_segment3, 5);
$urltitle = rtrim($urltitle, “.php”);
$urltitle = strtr($urltitle, “_”, “-”);
$path = $year . “/” . $month . “/” . $date;
header(“HTTP/1.1 301 Moved Permanently”);
header(“Location: [url=http://www.subtraction.com/”]http://www.subtraction.com/”[/url] . $path . “/” . $urltitle);
exit();
?>

{/if}

Archives by Date relies on the Yearly Archives plugin by Lodewijk Schutte. For the most recent three months the titles of the posts are displayed as well as the post count, after which the listing recedes to just a post count. These first three are separated from the others using a counter and a conditional, and only if the counter is less than 4 do we embed the template /embeds/archives-date (it must be embedded because the weblog:entries tag won’t work within the yearly_archives plugin):

&lt;p&gt;&#123;exp:weblog:entries weblog="subtraction|elsewhere|lens" dynamic="off" year="&#123;embed:year&#125;" month="&#123;embed:month&#125;" disable="categories|category_fields|custom_fields|member_data|pagination|trackbacks"&#125;
&lt;a href="&#123;path=&#123;entry_date format="&gt;&#123;title&#125;&lt;/a&gt;&nbsp;+&nbsp;
&#123;/exp:weblog:entries&#125;&lt;/p&gt;

This small template, having received the year and month, displays the month’s post titles and links to them. Since only the posts’ titles and URL titles are used, everything else in the weblog:entries tag is disabled. Each but the latest post is preceded by a “+”. Again the links to individual entries are in the blog-standard year/month/date format.

The rest of the months are tested to determine if they are December, and if so a yearly heading is inserted. Note that all the action is within a conditional that tests whether the month actually has any entries, this to omit empty months.

Over on the right Archives by Category are displayed using the Category Count technique from the EE Wiki.

Legacy URLs

All this is set to display only if the URL’s segment #2 is empty. If it’s not then we’re on a legacy URL from MoveableType, such as /archives/2005/1025_european_vac.php, and some PHP string manipulation at the end of the template converts this URL into one of our new ones. The year is retrieved from the original segment #2, the month from the first two characters of the original segment #3, the date from the next two characters, and the URL title from the 5th character on. The URL Title’s “.php” is chopped off then any underscores are converted to dashes. The result is /2005/10/25/european-vac, to which the browser is redirected. It’s great that the site had a consistent URL structure in its previous incarnation otherwise this would have been a lot harder.

The Footer Template

Now to the footer:

<div class=“clear”></div>

{if segment_1==”“ || segment_4 != “”}
<div id=“cat-table”> <h3>Categories</h3> <table cellspacing=“0” cellpadding=“0” border=“0”> <tr>

{!— Get the number of categories —} {exp:query sql=“SELECT count(cat_id) FROM exp_categories WHERE group_id=‘1’ GROUP BY cat_id”} <?php $count=”{count}” ?> {/exp:query} {!— Calculate the length of each column based on a manually-set number of columns —} <?php $cols = 4; $thiscol = 0; $collength= round($count / $cols); $offset=0; while ($offset < $count): ?> {!— Display a column —} <?php $thiscol++; if ($thiscol < 5): ?> <td> <ul> {embed=“embeds/footer-categories” count=”<?php echo $collength; ?<” offset=”<?php echo $offset; ?<”} </ul> </td> <?php endif; ?> <?php if ($thiscol < 4): ?><td class=“shim”></td><?php endif; ?> {!— Increase the offset by the length of a column and reiterate until done —} <?php $offset = $offset + $collength; endwhile; ?> </tr> </table> </div><!— /#cat-table —> {/if}

<div id=“footer”> <p><a href=“http://www.subtraction.com”>Subtraction.com</a> and all contents copyright 1998-{current_time format=”%Y”} by {encode=”[unencoded email address]?subject=Regarding Subtraction.com” title=“Khoi Vinh”}, unless otherwise noted.<br /> Contents under <a href=“http://creativecommons.org/licenses/by-nc-sa/1.0/”>Creative Commons License</a>. Visual design, layout and Cascading Style Sheets may not be reused without permission.</p>
</div><!— /#footer —>

</div><!— /home or articles —>
</body>
</html>

Khoi wanted his list of categories to be divided equally across four columns. First the number of categories is retrieved using a query tag and assigned to the PHP variable $count. Then the length of the columns, $collength, is calculated by dividing the number of categories by the number of columns, four. A loop displays each column. An offset value is also required so that the subsequent columns don’t merely repeat the first one. Since a PHP variable can’t be passed into a query tag due to the template parsing order, each column is displayed within an embedded template:

&#123;exp:query sql="SELECT count(exp_category_posts.entry_id) AS post_count,
exp_category_posts.cat_id, exp_categories.cat_name, exp_categories.cat_url_title
FROM exp_categories, exp_category_posts, exp_weblog_titles
WHERE exp_category_posts.cat_id = exp_categories.cat_id
AND exp_weblog_titles.weblog_id = '1'
AND exp_weblog_titles.entry_id = exp_category_posts.entry_id
GROUP BY exp_categories.cat_name LIMIT &#123;embed:count&#125; OFFSET &#123;embed:offset&#125;"&#125;
&lt;li&gt;&lt;a href="&#123;path=site_index&#125;categories/&#123;cat_url_title&#125;/"&gt;&lt;span&gt;&#123;cat_name&#125;&lt;/span&gt;&#123;post_count&#125;&lt;/a&gt;&lt;/li&gt;
&#123;/exp:query&#125;

Here again the Category Count technique is taken from the EE Wiki, with a limit and offset appended to the query, populated with values passed from the parent template.

We’re almost done, everything covered except the header:

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd”>

{assign_variable:my_template_group=“site”}
{assign_variable:my_weblogs=“subtraction|lens|elsewhere”}
<?php $css=”“; ?>

<html>
<head>
<title> Subtraction on Expression Engine{if “{segment_1}” ""} 7.0{if:else}:{/if} {if segment_1 != "categories"} {exp:weblog:entries limit="1" dynamic="off" url_title="{segment_4}" disable="categories|category_fields|member_data|pagination|trackbacks"} {!-- Article --}{if "{segment_4}" != ""}{title}{/if} {!-- Monthly --}{if "{segment_2}" != "" && "{segment_1}" != "categories" && "{segment_1}" != "archives" && "{segment_3}" “”}
{entry_date format=”%F %Y”} Archives
{/if} <?php $css=”{css}{elsewhere-css}”; $entryid=”{entry_id}”; ?> {/exp:weblog:entries} {/if} {!— Category —}{if “{segment_1}” "categories"}{exp:weblog:category_heading weblog="subtraction|lens|elsewhere" relaxed_categories="yes"}{category_name}{/exp:weblog:category_heading} Archives{/if} {!-- Archives --}{if "{segment_1}" “archives”}Archives{/if}
</title>
<link rel=“stylesheet” href=”{stylesheet=styles/main}” type=“text/css” />
<link rel=“stylesheet” href=”{stylesheet=styles/nav}” type=“text/css” />
<link rel=“shortcut icon” href=“favicon.ico” type=“image/x-icon” />
<?php if ($css != “”): ?>
<style>
<?php echo $css; ?>

</style>
<?php endif; ?>
<link rel=“alternate” type=“application/atom+xml” title=“Atom” href=“http://feeds.feedburner.com/subtraction” />
<!— script src=”/mint/?js” type=“text/javascript”>

//create jump menu
function jumpMenu(targ,selObj,restore){ //v3.0
eval(targ+”.location=’“selObj.options[selObj.selectedIndex].value”’”);
if (restore) selObj.selectedIndex=0;
}

</head>

<body>
<div id=”{if segment_1 ""}home{if:elseif "{segment_2}" != "" && segment_4 “” && “{segment_1}” != “categories” && “{segment_1}” != “archives” && “{segment_1}” != “about” } calendar {if:elseif “{segment_1}” "categories"} category {if:elseif "{segment_1}" “archives”} archives {if:elseif “{segment_1}” == “about”} about {if:elseif segment_4 != “”} articles {/if}
“>
<div id=“header”> <h1><a href=“http://www.subtraction.com”>Subtraction</a></h1> <p><img src=“http://www.subtraction.com/assets/subtraction_logo_black.gif” alt=”+S” width=“23” height=“23” /> Version 7.1 Beta – This dev server page {elapsed_time} secs<br /><a href=”{path=about}”>Khoi Vinh</a>’s Web Site</p> <h4>Search <span>via Google</span></h4> <div> <!— Google CSE Search Box Begins —> <form id=“searchbox_005794356726363419020:mq1sdd86pfu” action=“http://www.subtraction.com/search/”> <input type=“hidden” name=“cx” value=“005794356726363419020:mq1sdd86pfu” /> <input type=“hidden” name=“cof” value=“FORID:9” />

<input name=“q” class=“search-field” type=“text” /> <input type=“submit” name=“sa” value=“Go” /> </form> <!— Google CSE Search Box Ends —> </form> </div> </div><!— /#header —>

<div id=“navcontainer”> <ul id=“navlist”>

<li class=“home”><a href=”{path=/}”>Home</a></li> <li><a href=”{path=archives}”>Archives</a></li> <li><a href=”{path=about}”>About</a></li> {if segment_1 != “” && segment_1 != “archives” && segment_4==”“ && segment_1 != “categories” } {!— Monthly index —} <li class=“short”></li> {exp:weblog:calendar weblog=“subtraction|lens|elsewhere” leading_zeroes=“yes”} <li id=“prev”><a href=”{previous_path=/}”>Previous</a></li> <li id=“next”><a href=”{next_path=/}”>Next</a></li> {/exp:weblog:calendar} {if:elseif segment_1 "" OR segment_1 “archives” OR segment_1 == “categories”} {!— Homepage, Archives and Categories pages —} <li id=“empty”></li> {if:elseif segment_4 != “”} {!— Article —} {embed=“embeds/next-previous” thisentry_entryid=”<?php echo $entryid; ?<”} {/if} </ul> </div><!— #navcontainer —>

<div class=“clear”></div>

Within the window title is a weblog:entries tag. If we’re on an article the tag displays the article title and captures the entry ID and entry-specific CSS, if any, to insert later in the template (for performance reasons we only want to invoke the weblog:entries tag once in the header). If we’re on a monthly page then the month and year are displayed in long format. Otherwise the weblog:entries tag is skipped, and if we’re on a categories page then the category_heading tag is called to display its name, or if we’re on the archives page then that is stated.

Using PHP the article’s unique CSS is displayed if it has any. Then after the body is declared the outer div is given its id based on the URL.

The Next and Previous links differ depending on the page type. If we’re on a monthly page they display the next and previous month using the calendar tag. If we’re on an article they display the next and previous article. The next and previous links for articles are a casualty of our blog-style URLs: they simply don’t work. After some trial and error we gave up and used the Query module instead:

&lt;li class="short"&gt;&lt;/li&gt;
&lt;li id="prev"&gt;
 &#123;exp:query sql="SELECT title, url_title, entry_date FROM exp_weblog_titles 
 WHERE entry_date &lt; '&#123;embed:thisentry_entrydate&#125;' AND status='open' 
 AND (weblog_id='1' OR weblog_id='2' OR weblog_id='4' OR weblog_id='5')
 ORDER BY entry_date DESC LIMIT 1"&#125;
 &#123;if title&#125;
 &lt;a href="&#123;path=&#123;entry_date format="&gt;Previous&lt;/a&gt;
 &#123;/if&#125;
 &#123;/exp:query&#125;
&lt;/li&gt;
&lt;li id="next"&gt;
 &#123;exp:query sql="SELECT title, url_title, entry_date FROM exp_weblog_titles 
 WHERE entry_date &lt; '&#123;embed:thisentry_entrydate&#125;' AND entry_date &lt;= '&#123;current_time&#125;' AND status='open' 
 AND (weblog_id='1' OR weblog_id='2' OR weblog_id='4' OR weblog_id='5')
 LIMIT 1"&#125;
 &#123;if title&#125;
 &lt;a href="&#123;path=&#123;entry_date format="&gt;Next&lt;/a&gt;
 &#123;/if&#125;
 &#123;/exp:query&#125;
&lt;/li&gt;

Since the writing of this article I’ve developed a plugin, Nearby Entries, that solves the problem of displaying next/previous entries when using the year/month/date URL format.

Stepping back, I’m honored to have worked on this site that I admire so much. Thanks Khoi for the opportunity.

Add-ons

The site uses the following EE add-ons:

Plugins:

Extensions

What’s with all the excellent Dutchmen?

Post a comment

Name:

Email:

Location:

URL:

Your comment:

Remember my personal information
Notify me of follow-up comments?

Please enter the word you see in the image below: