Your project must contain the svn trunk version of apostrophePlugin, or version 1.4 stable. The 1.0 stable release of apostrophePlugin is missing necessary supporting features for apostropheBlogPlugin.
In a new project, creating the database tables for the blog plugin is a natural part of the usual command:
./symfony doctrine:build --all --and-load
Again, the quickest route is to check out the 1.5 stable branch (or the trunk) of the sandbox project, which includes the blog plugin, in which case you can just install that according to the directions. Then you can just do the usual doctrine:build command and you're good to go.
However, the blog plugin can be added to an existing Apostrophe Symfony project. To do so you'll need to install the plugin via plugin:install or svn externals (we recommend the latter), enable it correctly, and add the relevant tables to your database without, of course, overwriting all of your existing data.
First make sure that apostropheBlogPlugin is enabled AFTER apostrophePlugin in your config/ProjectConfiguration.class.php file. Do NOT use enableAllPluginsExcept for this purpose. Enable the specific plugins you want, with the blog plugin listed after the main Apostrophe plugin. Otherwise certain class files will not be found.
Once you have enabled the plugin, you need to add the appropriate database tables. Fortunately the apostrophe:migrate task is automatically extended to support this when the blog plugin is present. Make sure you have built your model, form and filter classes and cleared your cache, BACK UP YOUR DATABASE, and then run the apostrophe:migrate task, taking care to specify the correct environment:
./symfony doctrine:build --all-classes ./symfony cc ./symfony apostrophe:migrate --env=dev
This task requires MySQL. If you are not using MySQL, use doctrine:build-sql to generate SQL commands in data/sql/schema.sql and review that file for tables beginning with a_blog.
Adding Permissions For Blog Post Editors
Those who are adding the blog plugin to older Apostrophe projects might not have the blog_admin and blog_author permissions in their sf_guard_permission table, or the corresponding entries in sf_guard_user_group_permission. For security reasons we do NOT automatically add these via the apostrophe:migrate task. However, you can add them after you have performed the above steps by visiting the permissions and groups dashboards while logged in as the superuser. Be sure to create blog_admin and blog_author permissions and add them to your admin and editor groups. To locate the dashboards, log in as the superuser, click on "Users," and then click on "Permissions Dashboard" and "Groups Dashboard" at the left.
Creating Your Engine Pages
You're almost there, but you still need to create "engine pages" where your blog posts and events will be displayed. Although there is a separate back end for editing and managing blog posts, creating and naming the front end pages is up to you.
The distinction between the back end blog administration page and the front end blog engine page can be a bit confusing. However it offers substantial benefits. The back end administration page has many navigational and editing tools that would be overwhelming if we tried to present them in the context of the front end. That's why we use separate pages for this purpose.
To create your front end engine pages for blog posts and upcoming events:
- Navigate to the home page
- Log in as admin
- Click "This Page," then "Add New Page," giving it the name "News" or "Blog" depending on your preference
- When the new page appears, click "This Page," then click the "Gear" icon
- Set "Page Engine" to "Blog"
- Click Save.
Now repeat these steps for the "Events" engine.
Note that if you do not want your blog and events engines pages to be public you can unpublish them via the page settings menu, accessed via the gear. If you anticipate inserting blog post slots into pages but don't want an "actual blog" that presents navigation to access all blog posts, this might be right for your needs.
Posting to the Blog
To post to the blog, click the "Blog" icon at the top of any page. Then click "New Post."
Give your post a title, then start adding slots, exactly as you would if you were creating a regular Apostrophe page. The body of your blog post is a full-fledged Apostrophe "area," so it can contain rich text, slideshows, video and other Apostrophe features.
Blog Post Categories
Notice that blog posts can be organized into categories. This feature is critical because it allows you to insert blog posts on the same subject via blog post slots elsewhere on the site. You can also create additional blog engine pages which are locked to a single category. To lock a blog engine page to a single category, first create the page, then switch it to the blog engine, and then select one or more blog post categories from the category selector that appears.
Blog Post Slots
apostropheBlogPlugin provides two good ways to insert blog posts into your regular pages. You can insert a single blog post with the "Blog Post" slot, or insert multiple recent blog posts with the "Blog Posts" slot.
The "Blog Post" slot invites you to search by title. Since all blog posts must have unique titles this is a very effective way to pick a post.
The "Blog Posts" slot asks how many of the most recent blog posts you wish to display, and also allows you to select specific categories. The use of categories allows you to present relevant content on any page without foreknowledge of what that content will be.
Blog Post Excerpt Options
By default, blog post slots display a short excerpt from the post along with the first image, if any, found in the post. You can adjust this behavior with the following options to the aBlog or aBlogSingle slot:
- maxImages determines the number of images extracted from the blog post. If it is greater than one, the images are presented as a click-to-advance slideshow.
- slideshowOptions can be used to override the behavior of the slideshow, using exactly the same options available to slideshow slots (see ManualDesignersGuide).
- excerptLength determines the maximum word count of the excerpt.
These options are often enough. But you can do more, including displaying the entire blog post as part of a regular CMS page. Read on for more possibilities.
Blog Configuration Options
It's possible to control the number of blog posts per page displayed on the public blog pages, as well as other options, via app.yml settings. Here are the defaults laid out as you might set them in app.yml:
all: aBlog: # Posts per page in the blog engine (public blog page). # For memory and CPU reasons we don't recommend setting # this much higher than 20 max_per_page: 20 # Max autocomplete suggestions when typing a post title # to select a post to display in a blog slot autocomplete_max: 10 # Title of RSS feed feed_title: [defaults to blog engine page title] author_email: [for feed; defaults to none] author_name: [for feed; defaults to none] # Enable Add This buttons for blog posts if set add_this: your-addthis-profile-id-or-old-school-username-here # if true, allow comments when enabled for individual posts # Comments are implemented via disqus unless you override # those partials at project level to use something else comments: false # If this is true and comments is also true and # disqus_shortname is set, use disqus for comments disqus_enabled: true disqus_shortname: your-disqus-shortname-here # disqus developer flag, useful for debugging disqus_developer: true # If this is true, allow comments to be toggled on and off for # individual posts when editing posts (default: false) allow_comments_individually: false # If this is true, allow comments on new posts when # allow_comments_individually is active (default: true) allow_comments_initially: true
NOTE: when you first enable app_aBlog_allow_comments_individually, all existing posts will NOT allow comments, so you will definitely want to run ./symfony apostrophe:allow-comments --yes if you have existing blog posts and want to allow comments on the majority of them. You can also use --no to shut off comments on existing posts.
Customizing Blog Templates
It is possible to override the default appearance of the single-post, single-event and blog and event index pages by overriding the showSuccess and indexSuccess partials of the appropriate modules at the project level, following the usual Symfony style. In addition, beginning in Apostrophe 1.5, you can introduce different sets of templates that can be selected by the administrator of the site when they create a blog or event page within the site.
These are configured in app.yml along with the page templates:
all: a: templates: # "a:" contains "ordinary page templates" a: default: Default Page home: Home Page # This is how you enable engines as page type choices. If the # engine supports alternate page templates you can specify more # than one entry for an engine aMedia: default: Media aBlog: default: Standard Blog alternate: Alternate Blog aEvent: default: Events
Notice that there are two templates listed for the aBlog engine. The template named default always renders the showSuccess and indexSuccess templates, following Symfony's normal behavior. However the template named alternate renders the showAlternate and indexAlternate templates, and so on. You can define as many alternate template sets as you need.
Individual blog posts also have a blog post template that determines their basic structure. For instance, "out of the box" the blog plugin allows for one-column and two-column blog posts. The two-column posts have a narrow second column which is great for associated images and videos.
You can add additional templates to your project by overriding the aBlog settings in app.yml. Note that you must begin by copying this entire section into app.yml (of course you should merge this under your existing all heading):
all: aBlog: templates: singleColumnTemplate: name: Single Column areas: ['blog-body'] twoColumnTemplate: name: Two Column areas: ['blog-body', 'blog-sidebar'] comments: false # add_this: punkave # profile id or old-style username for AddThis -- http://addthis.com aEvent: templates: singleColumnTemplate: name: Single Column areas: ['blog-body']
Once you have copied these settings to your own app.yml, you can add additional templates in addition to the one-column and two-column templates, or replace them entirely. The templates you specify will be the choices on the "Template" dropdown when creating a blog post. Please do not remove templates that are already in use for posts on your site. If you need to do that, first add your new templates and change the template setting for each existing post.
Note that you need to specify an array of Apostrophe area names that appear in your templates (the areas setting for each template seen above in this app.yml excerpt). This is used to implement the getText convenience method, which returns just the text of an entire blog post. This is not currently used in the core of the blog plugin but does come in handy in application-level code at times, so we recommend specifying a list of area names for each template.
You can customize the appearance of these templates when the blog post is seen in on the blog engine page, and even change the Apostrophe areas that make up the blog post, by overriding the aBlog/singleColumnTemplate and aBlog/twoColumnTemplate partials. And you can customize the appearance of blog posts when inserted as slots by overriding aBlog/singleColumnTemplate_slot and aBlog/twoColumnTemplate_slot. And you can override the templates for the RSS feed by overriding aBlog/singleColumnTemplate_rss and aBlog/twoColumnTemplate_rss.
If you are using the standard RSS feed templates, you can easily add thumbnails to your feeds:
all: aBlog: feedThumbnails: true
You can also customize that presentation quite a bit. Just keep in mind that many feed readers completely ignore styles.
These are the default settings (except of course that feedThumbnails defaults to false):
all: aBlog: feedThumbnails: true feedThumbnailStyles: "display: block; clear: left; float: left; width: 100px; margin: 10px" feedThumbnailWidth: 100 # false = scale the height proportionally feedThumbnailHeight: false # If height is not false, you can set 'c' instead to crop the center feedThumbailCrop: s
Often these options are enough. However, if you are inserting blog posts on pages with different page templates and different amounts of space available, or you wish to use teasers in some cases and full-fledged blog posts in others, you'll want the subtemplate option.
When you insert a blog post slot in a page template like this:
<?php a_slot('blogpost', 'aBlogSingle', array('subtemplate' => 'inMyTemplate')) ?>
The blog plugin will append _inMyTemplate (note the underscore) to the blog post's template name, instead of the usual _slot.
That is, if the blog post is a single column post, _singleColumnTemplate_inMyTemplate.php will be used, and if the blog post is a two column post, _twoColumnTemplate_inMyTemplate.php will be used.
You can also specify subtemplates for specific blog post templates:
<?php a_slot('blogpost', 'aBlogSingle', array('template_options' => array('singleColumnTemplate' => array('subtemplate' => 'inMyTemplate')))) ?>
There are two more ways to change the template used for a blog post. You can simply force the blog slot to use the same template that the blog engine page would use:
<?php a_slot('blogpost', 'aBlogSingle', array('full' => true)) ?>
In this case, _singleColumnTemplate.php is used, exactly as it would be on the engine page.
Note that this won't work well with our out of the box blog templates, which are designed to display wide posts on the blog engine page. But it may work well with your own overrides of those templates.
The last option is to simply force the use of a specific blog post template:
<?php a_slot('blogpost', 'aBlogSingle', array('template' => 'myTemplate') ?>
This forces the use of _myTemplate_slot.php (which still must be in your application level override of the aBlog/templates folder). _slot is still appended because were are simply requiring the blog plugin to act as if this post's template was myTemplate. The usual rules for what comes next still apply.
The template option is often useful if your blog post templates are always designed in such a way that the most important area always has the same name. This is true for our standard blog post templates: the single column template and the two column template both have an area named blog-body, so you can safely substitute the former for the latter if you don't mind giving up the presumably less important information in the second column.
Working With Events
Events are slightly different from blog posts. A blog post is an article, a piece of news or other content that should be published now or on a particular future date. An event, on the other hand, has a fixed start time and end time during which it will take place.
Blog posts are traditionally presented in reverse chronological order (newest first). Events, by contrast, are typically presented in calendar order (today's events first).
apostropheBlogPlugin provides separate back end administration and front end engine pages for events, and separate slots for inserting single events and groups of upcoming events. The same features that are available for blog posts can also be used to customize the appearance and behavior of events.
As with blog posts, you must first create an engine page for events before you can add them to your site. Once you've done that, you can begin adding upcoming events to the system.
When you visit the events engine page, which typically becomes the public-facing "calendar of upcoming events" for your project, you'll see upcoming events sorted by start date and time. Again, this is different from blog posts, which are presented in reverse chronological order.
Users can still navigate to past events using the provided links to browse by year, month and day.
Managing Editing Privileges in the Blog Plugin
By default, those who are members of the editors group will be able to write blog posts, and delete and edit their own posts.
Those who are members of the admin group will be able to edit and delete the blog posts of others, and also determine which editors are permitted to assign which categories to blog posts. Admins can also grant editors the right to edit specific posts written by others.
Under the hood, the blog plugin looks for the same permissions that are used to determine which users are potential webpage editors. That is, if you are a potential webpage editor, you are a potential blog author.
Apostrophe Search and the Blog Plugin
Good news: published blog posts and events are automatically integrated into Apostrophe's sitewide search feature. If you search for something that appears in a post or event, a link to that event will be included in the results.
Adding Blog Comments with Disqus
You may wonder why we didn't implement our own system for commenting on blog posts.
In a nutshell: it's easy to implement comments badly. It's not so easy to authenticate users using their system of choice and filter spam effectively. Third party services like Disqus do it brilliantly for free. So rather than reinvent the wheel we've provided the hooks for you to take advantage of them.
(At a later date we may implement our own comment system, for instance to meet the needs of those who are blogging for an intranet audience where Disqus is not an option. Disqus will probably always be a desirable solution for most.)
For Apostrophe 1.5 (Recommended)
In Apostrophe 1.5 we've made it incredibly easy to implement disqus in your apostrophe (1.5+) site.
All you have to do is add this block to your project app.yml:
aBlog: disqus_enabled: true disqus_shortname: yourdisqusshortname # If this is true, allow comments to be toggled on and off for # individual posts when editing posts (default: false) allow_comments_individually: false # If this is true, allow comments on new posts when # allow_comments_individually is active (default: true) allow_comments_initially: true
Just change the disqus shortname to the shortname you created when you signed up for disqus.
(if you started with our sandbox, this is probably already in your project app.yml, in that case just un-comment it and change the shortname)
This will enable comments in the show success of your blog posts. You will still need to edit some of your blog templates if you want to do something like show the comment count on blog post slots. There are instructions for that in the 1.4 instructions below, as well as in the Disqus help documents.
If you decide to switch on allow_comments_individually for an existing site, you may want to force all existing posts to allow or disallow comments:
./symfony apostrophe:allow-comments --yes
./symfony apostrophe:allow-comments --no
For Apostrophe 1.4
If you're using the 1.4 version of apostrophe, you will need to override a few blog templates to enable disqus. (This is more work, so we suggest moving to Apostrophe 1.5.)
Here's how to set up Disqus with the Apostrophe 1.4 blog plugin:
- Visit Disqus and set up an account.
- On the "Choose Install Instructions" page, click "Universal Code."
- Override the aBlog/showSuccess template at the app level. Copy the existing aBlog/showSuccess, and add the following inside this if ... endif block found at the end:
<?php if($aBlogPost['allow_comments']): ?><?php endif ?>
<?php if($aBlogPost['allow_comments']): ?> <?php include_partial('aBlog/disqus') ?> <?php endif ?>
We'll be making this easier for you soon by adding an empty aBlog/postFooter partial that you can override as needed.
4. Optionally, in your blog post template overrides (aBlog/singleColumnTemplate and aBlog/twoColumnTemplate, as well as any blog templates you have added), add the #disqus_thread suffix to blog post permalinks so they can display comment counts. Change this:
<h3 class="a-blog-item-title"> <?php echo link_to($a_blog_post->getTitle(), 'a_blog_post', $a_blog_post) ?> </h3>
<h3 class="a-blog-item-title"> <?php $url = url_for('a_blog_post', $a_blog_post) . '#disqus_comments' ?> <a href="<?php echo $url ?>"><?php echo $a_blog_post->getTitle() ?></a> </h3>
5. Paste the "embed code" provided by Disqus into your aBlog/disqus partial (this code is provided on the Disqus site).
6. Provide yourself a convenient way to access the Disqus comment administration page for each blog post by overriding the aBlogAdmin/list_actions partial. This is a Symfony admin generator partial. You can fetch it from your Symfony cache, but we've reproduced it here for your convenience. The new code is the second line. Note that you must replace "YOURDISQUSSHORTCODE" with your disqus short code as provided by Disqus:
<?php echo $helper->linkToNew(array( 'params' => array( ), 'class_suffix' => 'new', 'label' => 'New',)) ?> <li><?php echo link_to('Comments', 'http://YOURDISQUSSHORTCODE.disqus.com', array('class' => 'a-btn big alt', )) ?></li>
7. Add the Disqus "comment count code" just before the closing </body> tag of your layout.php. Again, this code is provided on the Disqus "universal code" installation instructions page, so you should copy the latest code given there.
Optionally, you can also put Disqus into "developer mode" by pasting this code just before the closing </head> tag of your layout.php:
8. Turn on support for allowing comments in app.yml. Note that if you have not yet overridden any of the aBlog settings you'll need to copy the entirety of plugins/apostropheBlogPlugin/config/app.yml into the all section of your application-level app.yml file. Then you can make this change:
all: aBlog: # Be sure to copy the rest of the aBlog settings from the plugin app.yml too! # Now change the comments setting comments: true
As with all changes to app.yml, don't forget the symfony cc command.
Once you have done this, blog authors and editors will be able to check the "allow comments" box for individual posts, which will then show Disqus comments.
After following these steps you should immediately have working Disqus comments on your site. Follow the documentation on the Disqus site if you wish to style Disqus to more closely match your site's style.
Adding Custom Blog Filter Criteria
Out of the box, events and blog posts can be filtered by:
In addition, if you are also using apostropheEntitiesPlugin, the blog presents filters by each entity type. For instance, if your project has subclasses of aEntity named Person, Organization and Project, then blog posts can be filtered by these as well.
Note that the filters only appear if there are at least two items of a given type to choose from which are actually linked to posts or events.
In many cases, using the entities plugin is the right solution for adding additional relationships and filters to the blog. It is by far the easiest way. But if you have custom requirements, it's possible to add more filters by following the steps that follow. In fact, the technique below is followed by apostropheEntitiesPlugin. You may refer to the apostropheEntitiesPluginConfiguration class as an example.
Additional filter criteria can be added via the aBlog.extraFilterCriteriaand aBlog.filterForEngineParams Symfony events. Make sure you read up on Symfony events if you have not already done so.
Extra filter criteria are used to filter posts and events by criteria unique to your project or plugin. These criteria are also used to retrieve a list of possible values to filter by for which at least one blog post or event exists. For example, you can filter events by author, and you can also obtain a list of authors who have at least one event - taking all of the non-author-related filters that may be present fully into account. (Author filters are already built in but it's a good example.)
The interface for presenting the filters is also implemented via this mechanism.
Note that for reasons of performance and expressiveness all SQL queries discussed here are straight MySQL, not DQL.
Responding to the aBlog.extraFilterCriteria event
The aBlog.extraFilterCriteria event is invoked using the Symfony event dispatcher's filter() method. Your listener receives an array of extra criteria, to which it should add additional entries if any and then return the array.
An example helps a lot here, so be sure to check out the source code of the apostropheEntitiesPluginConfiguration class. You'll have a much easier time if you do.
Each entry in that array must be an associative array with the following keys:
filterKey. This will be expected as an option to the aBlogToolkit::filterForEngine() method, and employed in queries (see filterWhere, below). You'll convert URL parameters in the request into this option settings in your aBlog.filterForEngineParams event listener. Take care to name it so that it will not conflict with other parameters that may already be in a fairly complex query. For instance, product_id is excellent. id would not be a good choice.
arrayKey. This key in the returned results of aBlogToolkit::filterForEngine will be populated with the results of the select query for this criterion (see selectTemplate below). The aBlog/sidebar component relies on the presence of this data.
leftJoin contains SQL code to LEFT JOIN your table to the blog item (note that the id of the blog post or event is called "bi.id" in the query). This is added to every query to allow both filtering and selecting. This string may contain two consecutive left joins, if needed to use an intermediate table for a many-to-many relationship. You MUST NOT use an INNER JOIN. Instead you will use the filterWhere key to eliminate unwanted results in those cases where the user really is filtering on your custom criteria, and an AND clause in the selectTemplate key to discard rules where the id of your table is null in those cases where we are retrieving a list of items in your table.
filterWhere contains a SQL clause to filter the results when the user has chosen to use your custom filter. Queries to retrieve blog posts and events, list categories associated with them, list tags associated with them and so on are all restricted by the addition of this clause. The parameter appearing here should match filterKey and be preceded by a :. This clause is appended to a WHERE statement and is one among many clauses bound together by AND statements. Note that this clause is not used unless the user is actually filtering by author (assuming that's what our custom filter does), and is never applied when retrieving a list of authors since that would prevent you from discovering other authors.
selectTemplate decorates the main query with a SELECT clause and, where desirable, ORDER BY, HAVING and GROUP BY clauses as needed when retrieving a list of authors (if that's what our filter is intended to do). This is done to obtain a list of authors to display to the user so that they can filter by any author. The selectTemplate contains a %QUERY% placeholder which is replaced with the main body of the SQL query, including any non-author-related filters already in effect. Your template may assume that %QUERY% ends with a WHERE clause with at least one criterion bound together by AND keywords already present, so you can add an additional AND, typically to make sure the id of your joined table is not null (remember, we used a LEFT JOIN clause, so in the case where we're selecting possible values we have to discard the rows with null ids).
urlParameter should be a query string parameter name to be used when filtering on this criterion. End users will see this in the address bar. It should have a name likely to be unique to your table. product is good. id is not.
urlColumn should be a column name in the results of the select query to be used in the query string. This can be any unique column that exists in the table, such as id or slug.
labelTemplate is used to list this filter in a sentence describing all currently active filters. The %value% placeholder will be replaced with the current value of the urlParameter. If labelValueCallback is also present it can be used to transform the urlParameter into something more attractive, such as a full name.
labelValueCallback is optional. If present it is invoked to get a prettier name than just the urlParameter. It is passed the value of urlParameter. For instance, you might look up the author's full name in preference to their username, although the latter may be unique and thus more suitable for URLs and queries. Any valid PHP callback is acceptable. In PHP 5.3 and above you can use an anonymous function.
sidebarPartial or sidebarComponent contains the name of a Symfony partial or component to be used to render the list of possible values for this criteria as filter links. That partial will receive an 'items' parameter. Each entry in that array is an associative array containing all of the columns returned by the select query, plus a 'filterUrl' key containing a Symfony URL that will filter posts/events by this particular value (you must call url_for or link_to with it). For the item the user is already filtering on the filterUrl will remove the filter instead. When using sidebarComponent you must pass an array with two elements, the module name and the component name. When using sidebarPartial you pass a "module/partial" string. This mirrors the difference between include_partial and include_component and avoids an unnecessary string splitting operation. For a good example of a sidebarComponent that does some nontrivial work and a partial that formats the list properly to fit in with the rest of the sidebar, see the aEntitiesBlog module in the entities plugin.
Again, check out the apostropheEntitiesPluginConfiguration class for a complete example. You don't want to wing this without the benefit of a good reference.
Responding to the aBlog.filterForEngineParams event
You must also respond to the aBlog.filterForEngineParams event to convert request parameters into options for the aBlogToolkit::filterForEngine() method. These events are separate because it is possible to use the aBlogToolkit::filterForEngine() method outside the context of a blog page, without an sfRequest object.
This event listener receives an $options associative array as its second parameter. Your task is to fetch parameters from the current request object and store them in the $options array, possibly under different names. You must fetch request parameters under the same name you specified for urlParameter when adding your extra criterion, and store options under the same name you specified for filterKey.
Then simply return the options array.
Again, check out the apostropheEntitiesPluginConfiguration class for a complete example.
Including the Custom Filters In Your Subnav Layout
There is a partial, aBlog/subnavLayout, that determines the order in which the various filters are displayed. The default version of this partial already has an entry for entities in case you are using apostropheEntitiesPlugin:
<?php // This will harmlessly do nothing if you don't happen to have the entities plugin in your project ?> <?php include_slot('a_blog_sidebar_entities') ?>
Since you are adding your own custom criteria you will need to override this partial at project level and add include_slot lines for your criteria as well. The blog plugin automatically takes care of wrapping the results of your criterion's sidebar partial or component in a Symfony slot. The name of the slot will end with the arrayKey of your custom criterion.