If you've done any search engine optimization work you know about keywords. Keywords don't do much directly for users, but are included as meta data to help search engines understand what the designer/developer/content producer thought was important about the page being indexed. Anyone who works in SEO will tell you that relevant content is king. Keep your content relevant, link to relevant sources and provide relevant meta data. What do I mean by relevant meta data? I mean 3 meta tags that should be included on all pages: title, description and keywords. These 3 components can provide a lot of information about each individual page to the search engine spiders.

One of the things I noticed after installing BlogCFC and setting up this site was that I could set and update keywords for the entire site. Knowing the importance of those keywords, I thought this was great and during setup gave a few generic keywords to be used. But then I realized an issue: my blog is not about 1 specific topic so 1 set of keywords could actually prove to be a negative!

If you have delved into Google's webmaster tools you may have seen reports that show pages that have duplicate keyword content. Google, and I dare to guess Bing, Yahoo and Blekko. apparently don't like it when every page on your site has the same keywords. And that stands to reason; is EVERY page about the EXACT same thing? Probably not. So, I set about rectifying this issue in my BlogCFC installation. Several hours after I did it, when I couldn't get to sleep, I decided to write-up how I did it. This is my first attempt at writing about my coding adventures. I hope I didn't forget anything. I'm going to break this into 2 parts. This part will cover the admin side and part 2 will cover the implementation on the client side.

First things first, I needed a place to store my new keywords, so I went to the database (SQL Server 2005 in my case) and in the tblblogentries table, added a nullable, nvarchar(200) field named pageKeywords. It is important to make it nullable because no previous entries will have a value for this field.

Data storage out of the way, I had to populate it! this is my first foray into the BlogCFC code, so I had to do a little looking around. IN short order I discovered that I would need to amend 2 functions in org.camden.blog.cfc and then update admin/entry.cfm to ask for the data and pass it along to the functions.

Update addEntry function The first function I updated was addEntry. I'm not going to paste the whole function as the modifications were minor and should be easy to do should you choose. I'm passing in some new data, so I added a new parameter

view plain print about
1<cfargument name="pageKeywords" type="string" required="false" default="">

I also created a var-scoped local variable. I know CF9 offers the local scope, but I'm running CF8.

view plain print about
1<cfset var final_pageKeywords = application.blog.getProperty("blogKeywords")>

Because I'm going to add a text field to the form, argument.pageKeywords should always be defined. I went ahead and assigned the local variable final_pageKeywords the default, site-wide values. this way, if I decide not to specify distinct keywords, the data will still be populated. This has the benefit of presenting the list to me later should I choose to edit the entry, but let's not get ahead of ourselves.

Then I added the following snippet:

view plain print about
1<cfif listlen(arguments.pageKeywords,',')>
2     <cfset final_pagekeywords = arguments.pageKeywords>
3</cfif>
I hope this is self-explanatory. The keywords should come in as a comma-delimited list. If there are any entries passed in via arguments.pageKeywords, override what we previously assigned final_pagekeywordswith that list.

At this point, I considered appending or prepending my desired keywords into the preassigned final_pagekeywords to make a more robust list. I decided against it as I want to make sure my keywords are relevant to this page.

Finally, I updated the insert query to the following, which just added the pageKeyword field and final_pagekeyWords value:

view plain print about
1<cfquery datasource="#instance.dsn#" username="#instance.username#" password="#instance.password#">
2            insert into tblblogentries(id,title,body,posted
3                <cfif len(arguments.morebody)>,morebody</cfif>
4                <cfif len(arguments.alias)>,alias</cfif>
5                ,username,blog,allowcomments,enclosure,summary,subtitle,keywords,duration,filesize,mimetype,released,views,mailed,pageKeywords)
6            values(
7                <cfqueryparam value="#id#" cfsqltype="CF_SQL_VARCHAR" maxlength="35">,
8                <cfqueryparam value="#arguments.title#" cfsqltype="CF_SQL_VARCHAR" maxlength="100">,
9                <cfif instance.blogDBTYPE is "ORACLE">
10                    <cfqueryparam cfsqltype="cf_sql_clob" value="#arguments.body#">,
11                <cfelse>
12                    <cfqueryparam value="#arguments.body#" cfsqltype="CF_SQL_LONGVARCHAR">,
13                </cfif>
14
15                <cfqueryparam value="#arguments.posted#" cfsqltype="CF_SQL_TIMESTAMP">
16                <cfif len(arguments.morebody)>
17                    <cfif instance.blogDBType is "ORACLE">
18                        ,<cfqueryparam cfsqltype="cf_sql_clob" value="#arguments.morebody#">
19                    <cfelse>
20                        ,<cfqueryparam value="#arguments.morebody#" cfsqltype="CF_SQL_LONGVARCHAR">
21                    </cfif>
22                </cfif>
23                <cfif len(arguments.alias)>
24                    ,<cfqueryparam value="#arguments.alias#" cfsqltype="CF_SQL_VARCHAR" maxlength="100">
25                </cfif>
26                ,<cfqueryparam value="#getAuthUser()#" cfsqltype="CF_SQL_VARCHAR" maxlength="50">,
27                <cfqueryparam value="#instance.name#" cfsqltype="CF_SQL_VARCHAR" maxlength="50">,
28             <cfif instance.blogDBType is not "MYSQL" AND instance.blogDBType is not "ORACLE">
29                    <cfqueryparam value="#arguments.allowcomments#" cfsqltype="CF_SQL_BIT">
30             <cfelse>
31                      <!--- convert yes/no to 1 or 0 --->
32                     <cfif arguments.allowcomments>
33                         <cfset arguments.allowcomments = 1>
34                     <cfelse>
35                         <cfset arguments.allowcomments = 0>
36                     </cfif>
37                    <cfqueryparam value="#arguments.allowcomments#" cfsqltype="CF_SQL_TINYINT">
38             </cfif>
39                 ,<cfqueryparam value="#arguments.enclosure#" cfsqltype="CF_SQL_VARCHAR" maxlength="255">
40                ,<cfqueryparam value="#arguments.summary#" cfsqltype="CF_SQL_VARCHAR" maxlength="255">
41                ,<cfqueryparam value="#arguments.subtitle#" cfsqltype="CF_SQL_VARCHAR" maxlength="100">
42                ,<cfqueryparam value="#arguments.keywords#" cfsqltype="CF_SQL_VARCHAR" maxlength="100">
43                ,<cfqueryparam value="#arguments.duration#" cfsqltype="CF_SQL_VARCHAR" maxlength="10">
44                 ,<cfqueryparam value="#arguments.filesize#" cfsqltype="CF_SQL_NUMERIC">
45                 ,<cfqueryparam value="#arguments.mimetype#" cfsqltype="CF_SQL_VARCHAR" maxlength="255">
46                 ,<cfif instance.blogDBType is not "MYSQL" and instance.blogDBType is not "ORACLE">
47                    <cfqueryparam value="#arguments.released#" cfsqltype="CF_SQL_BIT">
48             <cfelse>
49                      <!--- convert yes/no to 1 or 0 --->
50                     <cfif arguments.released>
51                         <cfset arguments.released = 1>
52                     <cfelse>
53                         <cfset arguments.released = 0>
54                     </cfif>
55                    <cfqueryparam value="#arguments.released#" cfsqltype="CF_SQL_TINYINT">
56             </cfif>
57                ,0
58                ,<cfif instance.blogDBType is not "MYSQL" AND instance.blogDBType is not "ORACLE">
59                    <cfqueryparam value="false" cfsqltype="CF_SQL_BIT">
60             <cfelse>
61                    <cfqueryparam value="0" cfsqltype="CF_SQL_TINYINT">
62             </cfif>
63                ,<cfqueryparam cfsqltype="cf_sql_varchar" value="#final_pagekeywords#" maxlength="200">
64 )
65        </cfquery>

Updating saveEntry function While in blog.cfc, I made similar updates to the saveEntry function. I'll only post the line I added to the update sql statement to save time. I copied/pasted the code from addEntry to add the arguments.pageKeywords parameter, the var-scoped final_pageKeywords and the if-statement to determine the value for frinal_pageKeywords.

Add the line below to end of the update statement, just like any other value (don't forget the comma at the beginning of the line).

view plain print about
1,pagekeywords = <cfqueryparam cfsqltype="cf_sql_varchar" value="#final_pagekeywords#" maxlength="200">

Update admin/entry.cfm Entry.cfm does some processing at the top to allow for displaying values that have already been entered. So, I needed to define a new variable for the page to hold my keywords when the page loads pre-populated with data. Actually I had to do it twice because variables are defined based on whether or not the entry has already been saved or not. For simplicity here is the entire if-else statement that includes my additions.

view plain print about
1<cfif url.id neq 0>
2        <cfset entry = application.blog.getEntry(url.id,true)>
3        <cfif len(entry.morebody)>
4            <cfset entry.body = entry.body & "<more/>" & entry.morebody>

5        </cfif>
6
7        <cfparam name="form.title" default="#entry.title#">
8        <cfparam name="form.body" default="#entry.body#">
9        <cfparam name="form.posted" default="#entry.posted#">
10        <cfparam name="form.alias" default="#entry.alias#">
11        <cfparam name="form.allowcomments" default="#entry.allowcomments#">
12        <cfparam name="form.oldenclosure" default="#entry.enclosure#">
13        <cfparam name="form.oldfilesize" default="#entry.filesize#">
14        <cfparam name="form.oldmimetype" default="#entry.mimetype#">
15        <cfparam name="form.released" default="#entry.released#">
16        <cfparam name="form.duration" default="#entry.duration#">
17        <cfparam name="form.keywords" default="#entry.keywords#">
18        <cfparam name="form.subtitle" default="#entry.subtitle#">
19        <cfparam name="form.summary" default="#entry.summary#">
20         <cfparam name="form.pageKeywords" default="#entry.pageKeywords#">
21
22        <cfif form.released>
23            <cfparam name="form.sendemail" default="false">
24        <cfelse>
25            <cfparam name="form.sendemail" default="true">
26        </cfif>
27        
28        
29        <!--- handle case where form submitted, cant use cfparam --->
30        <cfif not isDefined("form.save") and not isDefined("form.preview")>
31            <cfset form.categories = structKeyList(entry.categories)>
32            <cfset variables.relatedEntries = application.blog.getRelatedBlogEntries(url.id, true, true, true)>    
33            <cfset form.relatedEntries = valueList(relatedEntries.id)>
34        </cfif>
35        
36        
37    <cfelse>
38    
39        <!--- look for savedtitle, savedbody from cookie, but only if not POSTing --->
40        <cfif not structKeyExists(form, "title") and structKeyExists(cookie, "savedtitle")>
41            <cfset form.title = cookie.savedtitle>
42        </cfif>
43        <cfif not structKeyExists(form, "body") and structKeyExists(cookie, "savedbody")>
44            <cfset form.body = cookie.savedbody>
45        </cfif>
46            
47        <cfif not isDefined("form.save") and not isDefined("form.return") and not isDefined("form.preview")>
48            <cfset form.categories = "">
49        </cfif>
50        <cfparam name="form.title" default="">
51        <cfparam name="form.body" default="">
52        <cfparam name="form.alias" default="">
53        <cfparam name="form.posted" default="#dateAdd("h", application.blog.getProperty("offset"), now())#">
54        <cfparam name="form.allowcomments" default="">
55        <cfparam name="form.oldenclosure" default="">
56        <cfparam name="form.oldfilesize" default="0">
57        <cfparam name="form.oldmimetype" default="">
58        <!--- default released to false if no perms to release --->
59        <cfparam name="form.released" default="#application.blog.isBlogAuthorized('ReleaseEntries')#">
60        <cfparam name="form.duration" default="">
61        <cfparam name="form.keywords" default="">
62        <cfparam name="form.subtitle" default="">
63        <cfparam name="form.summary" default="">
64        <cfparam name="form.relatedEntries" default="">
65        <cfparam name="form.sendemail" default="true">
66 <cfparam name="form.pageKeywords" default="">
67    </cfif>

I named the variable form.pageKeywords and if the field has already been populated, I set it to #entry.pageKeywords#, otherwise it is set to blank.

When the form is submitted, I need to tell it to pass this new value to our functions addEntry (for first time saving) and saveEntry (after making changes). A quick search of entry.cfm for 'addEntry' finds the following line and you will see that I have just added our value pageKeywords to the end of the parameters list, recalling that this parameter has already been defined in blog.cfc.

view plain print about
1<cfset url.id = application.blog.addEntry(form.title, form.body, moreText, form.alias, form.posted, form.allowcomments, form.oldenclosure, form.oldfilesize, form.oldmimetype, form.released, form.relatedentries, form.sendemail, form.duration, form.subtitle, form.summary, form.keywords, form.pageKeywords)>

Another quick search for 'saveEntry' yields the following line just above the addEntry code similarly updated.

view plain print about
1<cfset application.blog.saveEntry(url.id, form.title, form.body, moreText, form.alias, form.posted, form.allowcomments, form.oldenclosure, form.oldfilesize, form.oldmimetype, form.released, form.relatedentries, form.sendemail, form.duration, form.subtitle, form.summary, form.keywords, form.pageKeywords )>

Now I just needed to actually update the form to get the information for our functions. In entry.cfm, search for this comment:

view plain print about
1<!--- tab 2 --->

That will put you right at the top of the "Additional Settings" tab. I then copied and pasted one of the existing divs into the outer div like so:

view plain print about
1<div id="additional">    
2            <fieldset class="inlineLabels">
3 <div class="ctrlHolder">
4                <label for="pagekeywords">Page Keywords: </label>
5                <input type="text" name="pagekeywords" id="pagekeywords" value="#form.pagekeywords#" maxlength="200" class="textInput">
6 </div>
7 <div class="ctrlHolder">
8.
9.
10.
I then changed the names of the innocent div and form components to correspond to my new need: pageKeywords. Easy enough.

These simple changes will now save and update new page independent keywords for your entries. In part 2, I will describe the much simpler code I used to actually display the new keywords n my pages while maintaining the site-wide keyword list for non-entry pages.