invalid command name "ns_adp_include"
    while executing
"ns_adp_include header.adp "
    invoked from within chunk: 2 of adp: /home/dvr/web/dvr/cmd/templates.adp

Content creators will edit content through forms that provide structure to their content. If every story should have a headline, subhead, and body, the writers will use a form that looks like:

Headline
Subhead

To produce HTML, the structured data will be combined with a template. A simple template for a story might be:

<h2><%= $headline %></h2>

<h4><%= $subhead %></h4>

<p><%= $body %>

Combining the data and template generates:

Tigers beat Cougars 5-2

Late-inning home run by Smith

It was raining when the Tigers took the field...

Templates have two advantages. First, they separate the content from the layout, allowing the content creator and designer to each work on their own parts. Second, templates can be reused, which prevents duplication and enforces consistency. If all stories use the same template, and the designer wants to update the look of all stories on the site, he only needs to update one file. Since they all use the same template, he knows they'll all look the same.

A virtual file system

Most stories will have a few images to be displayed within the text of the story. A photo editor uploads the images, and the writer chooses which are used and where each appears within the story.

This creates two problems. First, how does the writer know which images were uploaded for his story? Second, how does the writer include links to these images?

If the webmaster of a site without a CMS wanted to publish a story with a few images, he would start by creating a new directory, then put the story and all its images in that directory. Since the images would be in the same directory as the HTML, he could include an image with a relative link:

It was raining when the Tigers took the field... 

<p><img src="rainy-field.jpg">

This is such a natural way to organize content that, even though our content is stored in the database, we want to recreate it. The CMS will provide a "virtual file system" where each item looks like a file, and items can be grouped together by being put in the same directory.

A major benefit of this approach is that relative URLs work exactly as they did before the CMS. If there is a story about American presidents that should include an image of FDR, the image can be assigned a URL in the same directory as the story, then included in the story with this HTML:

<p>Franklin D. Roosevelt was born in 1882...

<p><img src="fdr.gif">

At the data model level, supporting the virtual file system is straightforward. First we need a table with one row for every directory:

create table cm_directories (
  directory_id          integer 
                        constraint cm_directories_pk
                        primary key,
  parent_directory      -- for directories inside other directories
                        integer 
                        constraint cm_directories_parent_fk
                        references cm_directories,
  directory_name        -- since this will be part of a URL, we need to 
                        -- be sure this name doesn't have any spaces in it.
                        varchar(200) not null
);

Then we add directory_id and filename to cm_items:

create table cm_items (
  item_id               integer primary key,
  ...
  directory_id          integer references cm_directories,
  filename              varchar(200), 
  unique(directory_id, filename),
  ...
);

The unique constraint across (directory_id, filename) ensures that two items aren't assigned the same location in the virtual file system.

Do writers need to know HTML?

The dream is to have the content creator write only plain text, and have all the HTML in the template, but this doesn't work in practice. If a word inside a paragraph of a story needs to be bold, or in italics, or underlined, including a little HTML is the only solution.

How much HTML should content creators know? They should know a few basic tags: P, BR B, EM, U, HREF, IMG, and list operators: UL, OL, and LI. If this is too much, they can author content with a browser plug-in that gives them a WYSIWYG interface that generates HTML. However, this subset of HTML is small enough that anyone who uses a computer regularly should be able to learn it quickly.

From time-to-time, more complicated HTML will find its way into the body of the story. Perhaps a table listing all the presidents needs to accompany the story, and the only solution that makes sense is giving the data to the designer and having him put the formatted HTML directly into the body of the story. However, this approach should only be used when it's a unique piece of HTML, and it's needed for only one story.

When embedding HTML is a bad idea

Much of the HTML that needs to be used inside stories will be needed repeatedly. For example, most stories will have a few images displayed within the text, and all these images should be displayed the same way -- most likely within an HTML table, with the image in one cell and the caption in another.

We could have the designer write the HTML for every image, but this is a bad solution for a few reasons. First, this doesn't satisfy our requirement of consistency. The designer might know the HTML by heart and be able to write exactly the same HTML each time, but eventually he'll want to change the formatting (increase the font size, change the alignment, etc.), and he would need to manually reformat all the images in the old stories. This isn't practical. Therefore, our requirement is to have something that functions like a template: a single piece of HTML that can used to format all images on the site. If the designer wants to update all images within stories, he only needs to make the change in one place.

Second problem: time. If every image needs to be formatted by the designer, the story needs to pass through his hands before the story can go live. Our goal is to remove the designer from the workflow as much as possible. For most stories, the only design step should be choosing the template -- a task the writer or editor can handle. The designer should only be called in for a small fraction of the stories published.

Third, standard HTML isn't good enough if we want our CMS to avoid generating dead links. If a story goes live but an image doesn't, perhaps because it hasn't yet been approved, we want to avoid the broken IMG link that would result.

Forth, the caption being formatted isn't part of the story; it belongs to the image, which is its own content item, with its own workflow and revision history. When the CMS renders the HTML for the live revision of the story, it fetched the live revision of the caption, generates the HTML for the caption, and inserts that HTML into the body of the story. This means the person editing the text of the story can't format the caption within the story because the caption isn't there.

Custom tags

We want the writer to choose which images are used and where they appear within the story, but neither he or the designer should need to write the HTML. The best solution is to create a set of "custom tags" that allow the writer to specify filename and position, then have the CMS generate the HTML. For example, if a writer wanted to include an image named fdr.gif in his story, he would write:

<p>Franklin D. Roosevelt was born in 1882...

<cm:image_with_caption filename="fdr.gif">

image_with_caption isn't HTML; it's a custom tag the CMS will replace with real HTML. The final output will be:

<p>Franklin D. Roosevelt was born in 1882...

<center>
<table>
  <tr>
    <td><img src="fdr.gif"></td>
  </tr>
  <tr>
    <td>Franklin D. Roosevelt</td>
  </tr>
</table>
</center>

After the body of the story is pulled from the database, but before the data is combined with the template, the CMS will find all occurrences of <cm:image_with_caption ...> and replace them with the appropriate HTML. The tag is really a function which will:

  1. Determine the item_id of the image with the filename rain-on-field.jpg.
  2. Determine if the story is being displayed in 'live' or 'preview' mode.
  3. Choose the correct revision for the image in this mode
  4. If a revision is found, it fetches the image's information and generates the HTML. If a revision doesn't exist (which would happen if we're in 'live' mode and the image isn't live), it returns nothing.
The public view of story with an image inside. Both the story and image are live.

The same as above, except the image is no longer live. Notice there's no way to know the image is missing.

The tag will also do some error-checking. If the writer specified an image that doesn't exist, he should see a warning when he previews the story. However, these warnings should only appear in preview mode (which only staff will see); if the story goes live while trying to include an image that doesn't exist, it should fail silently without letting the public viewer know anything is wrong.

The writer made a typo -- 'fdrr.gif' instead of 'fdr.gif' -- and the system points this out when the story is previewed.

The tag cm:image_with_caption is supported by the function cm_tag_image_with_caption. Here is the source:

function cm_tag_image_with_caption ($attribute_array) {

    // The tag for cm:image_with_caption

    $align    = $attribute_array['align'];
    $filename = $attribute_array['filename'];

    if ($filename == "") {
        return cm_tag_error("No filename specified");
    }

    // What directory are we in? This should have been set beforehand
    $directory_id = cm_page_info("directory_id");

    if ($directory_id == "") {
        // We have a filename, but we don't have a directory, so
        // we can't determine an item_id
        
        return cm_tag_error("No directory specified for $filename"); 
    }

    if (cm_in_preview_mode_p())
        $image_view = 'cmx_vlast_image';
    else
        $image_view = 'cmx_vlive_image';

    $db = db_open();

    $q = db_prepare($db, "
            select 
                url,
                caption
            from $image_view
            where filename = :filename
            and directory_id = :directory_id");

    db_bind($q, filename,     $filename);
    db_bind($q, directory_id, $directory_id);

    db_execute($q);

    if (db_getrow($q)) {
        // We have a revision to show

        foreach (db_colnames($q) as $colname) {
            // create variables $url, $image_width, etc...
            $$colname = db_getcol($q, $colname);
        }

        db_close($db);

        // Now that the variables exist, call the function that
        // generates the html.

        return cm_html_image_with_caption (cm_url_with_prefix($url), $caption, $align);

    } else {

        db_close($db);

        // There's no image to show. There are two reasons why this
        // might be the case: (1) the image isn't live, or (2) the
        // target image doesn't exist. If we're in preview mode, we
        // know it's #2, so send a warning.

        if (cm_in_preview_mode_p()) {
            return cm_tag_error("Image $filename not found in this directory");
        } 
    }
}
cm_tag_error ensure that errors are only shown in preview mode.
function cm_tag_error ($error) {

    // If we're content to the public and a tag has an error, we
    // want to hide it. If we're in preview mode, then it's only a
    // staff member viewing the page, and we want to bring the error
    // to their attention.

    if (cm_in_preview_mode_p()) {
        return "<div class=tagerror>$error</div>";
    }
}

Notice the call in cm_tag_image_with_caption to cm_html_image_with_caption. This generates the HTML for the image and caption.

The ideal solution is to have cm_tag_image_with_caption generate the HTML by parsing a template that's maintained by the designer. We only need a function which takes the path to a template and returns HTML.

The problem is that most language don't support this feature; PHP has an 'include' function, but this would execute the code in the template and write the output to the connection right away. The only solution is to write a function that generates the HTML:

function cm_html_image_with_caption ($url, $caption, $align) {

    if ($align == '')
        $align = center;

    if ($align == 'center') {

        return "
        <center>
        <p><table>
          <tr>
            <td><img src=$url></td>
          </tr>
          <tr>
            <td class=caption align=left>$caption</td>
          </tr>
        </table>
        </center>";
    
    } elseif ($align == 'left') {

        return "
        <center>
        <p><table>
          <tr valign=top>
            <td class=caption>$caption</td>
            <td><img src=$url></td>
          </tr>
        </table>
        </center>";

    } elseif ($align == 'right') {

        return "
        <center>
        <p><table>
          <tr valign=top>
            <td><img src=$url></td>
            <td class=caption>$caption</td>
          </tr>
        </table>
        </center>";

    } else {
        return cm_tag_error("'align' must be 'left', 'right', or 'center'");
    }
}
invalid command name "ns_adp_include"
    while executing
"ns_adp_include footer.adp "
    invoked from within chunk: 18 of adp: /home/dvr/web/dvr/cmd/templates.adp