Вы находитесь на странице: 1из 153

= Apostrophe Manual =

== Overview ==

Welcome to the 1.0 stable release of Apostrophe! Although this is our first official stable release, our
CMS is already in regular use on a number of production sites. This release reflects the fact that our
code has been stable and ready for your professional use for quite some time. So there's no need to
wait; let's get started building your site.

This document has nine chapters:

* [ManualInstallation Installation]

* [ManualUpgrade Upgrade]

* [ManualEditorsGuide Editor's guide]

* [ManualDesignersGuide Designer's guide]

* [ManualDevelopersGuide Developer's guide]

* [ManualBlogPlugin Blog plugin]

* [wiki:ManualI18N Internationalization guide]

* [ManualImportGuide Import & migration guide]

* [ManualDeployment Our deployment process]

Installation, the editor's guide, the designer's guide and the developer's guide are intended to be
read consecutively by those who intend to do it all on their own. Naturally you might divide these
tasks among your team, but the editor's and designer's material should be understood first before
tackling the developer's section.

The blog plugin is used to manage news and events and also to present timely content in other parts
of the site. The Internationalization guide is an optional extra for those who need to support content
in multiple languages or present clients with an editing interface in a language other than English.

Those who wish to migrate content from an existing CMS system will want to read the import &
migration guide.

The installation section is intended for developers and system administrators with some knowledge
of PHP and command line tasks. The upgrade section is appropriate for a similar audience and also
includes important notes for designers and developers who are affected by changes impacting
custom slots and CSS.

The editor's guide is suitable for end users who will have administrative responsibilities on the site,
such as editing and managing content. No programming skills required.

The designer's guide addresses the needs of front-end developers, discussing how to edit and
manage stylesheets, page templates and page layouts.

Finally, the developer's guide explores how to extend Apostrophe with new content slots that go
beyond our provided rich text and media features and new "engines" that extend the CMS with full-
page content that doesn't always fit neatly into the page metaphor. Most readers will never need to
refer to this section.

See the end of this document for information about community and professional support, the
Apostrophe community and how to participate in further development.

=== Guiding Philosophy ===

[http://www.apostrophenow.com/ Apostrophe] is a content management system. Apostrophe is

open source, and built upon the great work of other open source projects. That's why our
apostrophePlugin is a plugin for the [http://www.symfony-project.org/ Symfony] web application

The philosophy of Apostrophe is that editing should be done "in context" as much as possible,
keeping confusing modal interfaces to a minimum and always emphasizing good design principles
and an intuitive user experience. When we are forced to choose between ease of use and a rarely
used feature, we choose ease of use, or make it possible to discover that feature when you truly
need it.

Before we decided to write our own CMS, we used sfSimpleCMSPlugin, and although our system is
quite different you can see its influence in Apostrophe. We'd like to acknowledge that.

=== Who Should Use Apostrophe Today? ===

Right now Apostrophe is best suited to PHP developers who want to make an intuitive content
management system available to their clients. Apostrophe is very easy for your clients to edit,
administer and maintain once it is set up. Right now, though, Apostrophe installations does call for
some command line skills and a willingness to learn about Symfony. We are working to reduce the
learning curve.

Front-end developers who do not yet have PHP and Symfony skills but wish to set up an Apostrophe
site by themselves should consider tackling the [http://www.symfony-
project.org/jobeet/1_4/Doctrine/en/ Symfony tutorial] to get up to speed. It's not necessary to
complete the entire tutorial, but it helps to have at least a passing familiarity with Symfony.

And of course we at [http://www.punkave.com/ P'unk Avenue] are available to develop complete

Apostrophe sites for those who see the value in a truly intuitive CMS and do not have the
development resources in-house to implement it.

=== Apostrophe Features ===

Standard features of Apostrophe include version control for all content slots, locking down pages for
authenticated users only, and in-context addition, deletion, reorganization and retitling of pages.
When a user is logged in with appropriate privileges intuitive editing tools are added to the usual
navigation, neatly extending the metaphors already present rather than requiring a second interface
solely for editing purposes.

Apostrophe also introduces "areas," or vertical columns, which users with editing privileges are able
to create more than one slot. This makes it easy to interleave text with multimedia and other custom
slot types without the need to develop a custom PHP template for every page.

Content "slots" include plaintext, rich text, RSS feeds, photos, slideshows, videos, PDFs and raw
HTML slots.

Apostrophe includes support for media management, including a built-in media library that allows
you to manage locally stored photos and remotely hosted videos. When media are embedded in
pages they are automatically sized to cooperate with the page templates created by the designer.

Rich text editing, of course, is standard equipment. And unlike most systems, Apostrophe
intelligently filters content pasted from Word and other programs to ensure there are no design-
busting markup conflicts.

=== Supported Browsers ===

Editing works 100% in Firefox 2+, Safari 4+, Chrome and Internet Explorer 7+.

Editing is expressly not supported in Internet Explorer 6. Of course,

browsing the site as a user works just fine in Internet Explorer 6. Although IE 6 cannot support our
full editing experience, we recognize the need to support legacy browser use by the general public
when visiting the site.

=== System Requirements ===

Please see the [ManualInstallation Installation Guide] for a list of Apostrophe's system
requirements. A `servercheck.php` page is provided with the sandbox. This page can be accessed to
verify that your site meets the requirements.

== Support, Community and News Sources ==

Please be sure to join the [http://groups.google.com/group/apostrophenow apostrophenow Google

group]. This is the right place to obtain community support.

Bug tracking, subversion access and a community Wiki are all available at
[http://trac.apostrophenow.org trac.apostrophenow.org].

Professional support for Apostrophe is available from our team here at [http://www.punkave.com/
P'unk Avenue].

Also be sure to [http://twitter.com/apostrophenow follow our Twitter account].

For bleeding-edge development news, subscribe Google Reader or your feed reader of choice to our
svn commit notes:

[http://www.apostrophenow.com/svn.rss http://www.apostrophenow.com/svn.rss]

And of course, be sure to [http://www.apostrophenow.com/ visit the Apostrophe Now site].

Please continue reading with one of the following:

* [ManualInstallation Installation]

* [ManualEditorsGuide Editor's guide]

* [ManualDesignersGuide Designer's guide]

* [ManualDevelopersGuide Developer's guide]

= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Installation ==

There are two ways to get started with Apostrophe. You can start with our sandbox project, which
we heartily recommend, or add the apostrophePlugin to your existing Symfony project. The latter
only makes sense for experienced Symfony developers whose sites are already well underway. I'll
describe both approaches.

But first, let's make sure your webserver meets the requirements for Apostrophe.

== System Requirements ==

Apostrophe requires the following. Note that virtually all of the requirements are included in the
asandbox project which you can easily check out from svn, copy from svn to your own repository as
described below, or just download as a tarball.

The following must be installed on your system:

* PHP 5.2.4 or better, with a PDO driver for MySQL (we recommend PHP 5.3.3 or better if possible;
the latest in the 5.2.x series is also OK)

* MySQL (we will accept patches for greater compatibility with other databases provided they do
not damage MySQL support)

* For the media features: GD support in PHP, or the netpbm utilities. netpbm uses much less

* Optional, for previews of PDFs in PDF slots: ghostscript (you must also have netpbm to use this

* A few truly excellent hosting companies already have netpbm and ghostscript in place. If you have
a Virtual Private Server (and you should, shared hosting is very insecure), you can most likely install

netpbm and ghostscript with a few simple commands like sudo apt-get install netpbm and sudo apt-
get install ghostscript.

If you are choosing a Linux distribution, we recommend Ubuntu. Ubuntu includes a sufficiently
modern version of PHP right out of the box. If you are using Red Hat Enterprise Linux or CentOS, you
will need to upgrade PHP to version 5.2.x on your own. This is unfortunate and Red Hat really ought
to get a move on and fix it.

A `servercheck.php` page that verifies these requirements is included in the sandbox. Just visit that
page after adding the `web` folder of the sandbox to your server as the document root of a new site.

Mac users: want to build your site on your own computer first? Please say yes, this is the right way
to avoid surprises. You can most easily meet the web server and PHP requirements by installing

the latest version of [http://www.mamp.info/ MAMP]. Note that MAMP's PHP

must be your command line version of PHP, not Apple's default install of PHP.

To fix that, add this line to the `.profile` file in your home directory:


export PATH="/Applications/MAMP/Library/bin:/Applications/MAMP/bin/php5/bin:$PATH"


Of course your production server will ultimately need to meet the same requirements with regard to
PHP and PDO.

Apple's default version of PHP for Snow Leopard is theoretically capable of working with
Apostrophe, but unfortunately Apple chose to ship the very first bleeding-edge version of PHP 5.3
and as of this writing they have not updated it to address the many PHP bugs since discovered and
fixed in the PHP 5.3 series.

Yes, you can also use Apostrophe with Microsoft Windows as a hosting environment. Core
Apostrophe features do not depend on Unix-specific command line tools and services. We strongly
recommend that you use the Microsoft Web Platform PHP accelerator for reasonable performance
on Windows.

=== Ways to Download the Apostrophe Sandbox Project ===

==== Download The Tarball ====

The easiest way is to just [http://www.apostrophenow.com/downloads/asandbox-stable.tar.gz

download a tarball of the sandbox project.] This tarball is updated nightly from the stable branch of
our sandbox. This is a great way to get started, but note that you won't be able to get security and
stability fixes just by typing `svn update` this way.

==== Check It Out From Subversion ====

Alternatively you can check it out with subversion:


svn co http://svn.apostrophenow.org/sandboxes/asandbox/branches/1.4 asandbox


That will check out the 1.4 stable branch, which is suitable for use with both Symfony 1.3 and
Symfony 1.4.

If you prefer to live dangerously and use our most bleeding-edge code ('''as of 6/22 the trunk is
currently quite bleeding-edge and we would recommend using the 1.4 branch instead'''), you can
check out the trunk:


svn co http://svn.apostrophenow.org/sandboxes/asandbox/trunk asandbox


We love it when you do this, because it results in more feedback for us, but you should definitely
consider using the stable branch for your client projects. We use the stable branch for our own client

==== Our Favorite: Copying the Sandbox to Your Own Repository ====

Checking out the sandbox from svn is a nice way to get started, but you'll soon wonder how to save
your own changes when the project is part of our own svn repository. That's why we recommend
that you instead copy it to your own repository with
[https://sourceforge.net/projects/svnforeigncopy/ svnforeigncopy] each time you want to start a
new site. That's what we do. With `svnforeigncopy` you get a copy of the asandbox project in your
own svn repository, with the `svn:ignore` and `svn:externals` properties completely intact. You don't
get the project history, but since 99% of the code is in the externally referenced plugins and libraries,
that's really not a big deal. The important thing is that you are still connected to the latest bug-fix
updates for our plugin.

This will give you all of the necessary plugins and the ability to

`svn update` the whole shebang with one command.

Here's an example. In this case I have already copied the `svnforeigncopy` script to my personal
`~/bin` folder and made it executable with the `chmod` command. (That assumes `~/bin` is in your
PATH, which you can adjust in `~/.profile` or ~/.bashrc`. If this is all Greek to you, it might be better
to download the tarball.)

```NOTE:``` You need svn version 1.5 or better. MacOS X Snow Leopard includes a 1.6.x version of
svn, which is plenty good enough. So does Ubuntu Linux. If you don't have svn 1.5 or better, don't
worry about it, just install our tarball instead.

This command creates a new `svn` repository, of course you can use an existing one, such as a
beanstalk repository. For more information about svn see the [http://svnbook.red-bean.com/ official
svn red bean book]. Of course you should substitute your own preferred location for `/Users/boutell`


svnadmin create /Users/boutell/myrepo


```IMPORTANT:``` if you are making your own repository make sure you turn off `enable-rep-sharing`
to avoid this error:


svn: no such table: rep_cache


To prevent this, after you create your repository (see above), edit
`/Users/boutell/myrepo/db/fsfs.conf` and add this line:


enable-rep-sharing = false


This disables an optional optimization that can cause problems. Unfortunately it is turned on by
default in svn on MacOS X Snow Leopard. Thanks to Pablo Godel of !ServerGrove for pointing out
the issue.

Now, this command copies the project from our repository to yours:


svnforeigncopy http://svn.apostrophenow.org/sandboxes/asandbox/branches/1.4
file:////Users/boutell/myrepo/demosite Sites/demosite


Those arguments are the path you're copying from, the path you're copying to, and the local
checkout folder where you'll work with the end result (also used as a scratchpad during the copy

You'll be prompted to commit to svn a couple of times (you may need to set your VISUAL
environment variable if you have never done so before), and the checkout of symfony in lib/vendor

10 | P a g e
means it'll take a little while to do the final 'update' that brings down the related plugins and
Symfony itself. When it's over, though, you have our project cloned in your own repository where
you can commit your own changes without losing the connection to the stable branch of our plugin.

=== Apache Configuration ===

Once you have fetched the code by your means of choice, you're ready to configure your testing
web server to recognize a new website. Light up the folder `asandbox/web` as a website named
`asandbox` or whatever suits your needs via MAMP, `httpd.conf` or whatever your web hosting
environment requires. As with any Symfony project you'll want to allow full Apache overrides in this
folder. See `config/vhost.sample` for tips on virtual host configuration for Apache. Make sure the
directives in `web/.htaccess` will be honored by your server.

(Confused about this MAMP stuff? Wondering how you can test websites on your own computer?
You really should pick up [http://www.mamp.info/ MAMP] if you are on a Mac. Windows and Linux
users should consider using [http://www.apachefriends.org/en/xampp.html XAMPP]. These are all-
in-one packages with PHP, MySQL, Apache and everything else you need to test real websites on
your own computer. You can also install Apostrophe directly on a web hosting account. We just think
you'll find web development with Apostrophe and in general oh so much more pleasant if you learn
to test on your own computer before uploading things.)

=== PHP Configuration ===

There is a `php.ini` setting that determines whether parameters in URLs are separated by & or
by &. Apostrophe currently requires that they be separated by &.

In most web hosting setups, you can add this line to `web/.htaccess` of your Apostrophe project to
ensure that the separator is &:


php_value arg_separator.output &


If PHP directives are not honored in `.htaccess` files in your hosting setup, you can do this in your
`php.ini` file:

11 | P a g e

arg_separator.output = "&"


You must also have a reasonable PHP `memory_limit` setting. We recommend 64 megabytes of
memory for PHP:


memory_limit = 64M


A few Linux distributions set unrealistic memory limits on PHP by default. In this case you may get a
blank page when accessing Apostrophe sites.

Don't forget to restart Apache if you make changes in `php.ini`.

Note that without this rule content editing will not work properly in Apostrophe.

=== Symfony Configuration ===

Yes, this is a preconfigured sandbox project, but you do have to adjust a few things to reflect reality
on your own computer.

Now create the `config/databases.yml` file, which must contain

database settings appropriate to ''your'' system. Copy the file

`config/databases.yml.sample` as a starting point:


cp config/databases.yml.sample config/databases.yml

12 | P a g e

If you are testing with MAMP the default settings

(username root, password root, database name asandbox) may work

just fine for you. If you are testing on a staging server you will

need to change these credentials.

Also create your `properties.ini` file:


cp config/properties.ini.sample config/properties.ini


In Symfony `properties.ini` contains information about hosts that a project can be synced to, in

to the name of the project. The sample properties.ini file just defines the name of the project. You'll

add information there when and if you choose to sync the project to a production server via
`project:deploy` or our enhanced version, `apostrophe:deploy`. See the Symfony documentation for
more information about that technique.

At this point you're ready to use the checkout of Symfony's 1.4.x stable branch

that is included in the project. If you want to use a different installation

of Symfony, such as a shared install for many sites (note that only 1.3.x and 1.4.x are likely to work),
copy config/require-core.php.example to config/require-core.php and edit the

paths in that file.

Next, cd to the `asandbox` folder and run these commands.

13 | P a g e
(Windows users: you should remove the `web/apostrophePlugin`, `web/apostropheBlogPlugin`, etc.
folders first. These are the subfolders in `web` that end in `Plugin`. Unix/Linux users don't have to
worry about this.)


./symfony cc

./symfony plugin:publish-assets

./symfony doctrine:build --all

./symfony doctrine:data-load


This will create a sample database from the fixtures files.

If you prefer, you can pull down our full demo site as an alternative to the somewhat bland fixtures
site. Replace the `doctrine:data-load` command with this one (it's OK to do this if you already did the
other command):


./symfony apostrophe:demo-fixtures


Note that this task will run for quite a while as media files are included in the download.

Now set the permissions of data folders so that they are writable by the web

server. Note that svn does NOT store permissions so you can NOT assume they are

already correct:


./symfony project:permissions

14 | P a g e

Our apostrophePlugin extends project:permissions for you to include the

data/writable folder in addition to the standard web/uploads, cache and log folders. Handy, isn't it?

If you prefer you can do this manually:


chmod -R 777 data/a_writable

chmod -R 777 web/uploads

chmod -R 777 cache

chmod -R 777 log


More subtle permissions are possible. However be aware that most

"shared hosting" environments are inherently insecure for a variety

of reasons. Production Symfony sites should run on a virtual machine of their own, or share a VM
only with other sites written by you. So before criticizing the "777 approach," be sure

to [http://trac.symfony-project.org/wiki/SharedHostingNotSecure read this article on shared hosting

and Symfony].

Next, build the site's search index for the first time (yes, search is included). It doesn't live in

the database so it needs to be done separately. After this, you won't

need to run this command again unless you are deploying to a new

environment such as a staging or production server and don't plan to sync your content with


./symfony apostrophe:rebuild-search-index --env=dev

15 | P a g e

(You can specify `staging` or `prod` instead to build the search indexes

for environments by those names. You'll want that later when working on a production server.)

'''You can now log in as `admin` with the password `demo` to see how the site behaves when you're
logged in''' (if you used the apostrophe:demo-fixtures task, the password will be `demo`). Start
adding subpages, editing slots, adding slots to the multiple slot content area... have a ball with it!


Note: For all versions of the demo site, the login is `admin` and the password is `demo`.


=== The Hard Way: Adding apostrophePlugin To An Existing Site ===

''Those who installed the sandbox just now can skip right over this section.''

Installing the sandbox is the easy way to install Apostrophe. The notes that follow assume you're
doing it the hard way, without starting from the asandbox project.

Begin by installing the following Symfony plugins into your Symfony 1.3/1.4 project:

* sfJqueryReloadedPlugin

* sfDoctrineGuardPlugin (4.0.x - version 5.0.x has undocumented backwards compatibility breaks

and no documentation of its new features - we cannot recommend it right now)

* sfDoctrineActAsTaggablePlugin

* sfWebBrowserPlugin

* sfFeed2Plugin

* sfSyncContentPlugin (recommended but not required)

* And of course, [http://www.symfony-project.org/plugins/apostrophePlugin apostrophePlugin]

16 | P a g e
We strongly encourage you to do so using svn externals.

'''For sfDoctrineGuardPlugin you currently must use the 4.0.x series. This is not what `plugin:install`
gives you by default.''' The 5.0.x series has been declared stable, but there is no documentation of
the many changes, including backwards compatibility breaks in the names of relations. So we cannot
recommend it yet.

If you are using svn externals to fetch plugins, pin your `svn:external` for sfDoctrineGuardPlugin to
the 1.3 branch:


Also, if you are using svn externals you will need to be sure to create the necessary symbolic links
from your projects web/ folder to to the web/ folders of the plugins that have one. Use the
`plugin:publish-assets` task, or create your own links with a relative path:


cd web

ln -s ../plugins/apostrophePlugin/web apostrophePlugin

# Similar for other plugins required


The search features of the plugin rely on Zend Search, so you must

also install the Zend framework.

[http://framework.zend.com/download/latest The latest version of the minimal Zend framework is

sufficient.] If you choose to install this system-wide

where all PHP code can easily find it with a `require` statement, great.

If you prefer to install it in your Symfony project's

`lib/vendor` folder, you'll need to modify your `ProjectConfiguration` class

17 | P a g e
to ensure that `require` statements can easily find files there:


class ProjectConfiguration extends sfProjectConfiguration

public function setup()

// We do this here because we chose to put Zend in lib/vendor/Zend.

// If it is installed system-wide then this isn't necessary to

// enable Zend Search


sfConfig::get('sf_lib_dir') .

'/vendor' . PATH_SEPARATOR . get_include_path());

// for compatibility / remove and enable only the plugins you want



Create an application in your project. Then create a

module folder named `a` as a home for your page templates

and layouts (and possibly other customizations):


mkdir -p apps/frontend/modules/a/templates


18 | P a g e
The CMS provides convenient login and logout links. By default these

are mapped to sfGuardAuth's signin and signout actions. If you are

using sfShibbolethPlugin to extend sfDoctrineGuardPlugin, you'll

want to change these actions in apps/frontend/config/app.yml:




domain: duke.edu


actions_logout: "sfShibbolethAuth/logout"

actions_login: "sfShibbolethAuth/login"


You can also log in by going directly to `/login`. If you don't want to display the login link

(for instance, because your site is edited only you), just shut that feature off:




login_link: false


You will also need to enable the Apostrophe modules in

your application's `settings.yml` file. Of course you may need

other modules as well based on your application's needs:

19 | P a g e



- aSync

- aNavigation

- aMedia

- aMediaBackend

- aRichTextSlot

- aTextSlot

- aRawHTMLSlot

- aSlideshowSlot

- aVideoSlot

- aImageSlot

- aButtonSlot

- aPDFSlot

- aFeedSlot

- sfGuardAuth

- aUserAdmin

- aGroupAdmin

- aPermissionAdmin

- sfGuardPermission

- taggableComplete

- aNavigation

- default

- aAdmin


20 | P a g e
Apostrophe edits rich text content via the FCK editor. A recent version of FCK is

included with the plugin. However you'll need to enable FCK in your settings.yml file, as follows:



rich_text_fck_js_dir: apostrophePlugin/js/fckeditor


==== Disabling Output Escaping ====

By default, a new Symfony 1.4 project escapes all output that is emitted in templates. Apostrophe's
strategy is to store valid UTF-8 encoded HTML to begin with, addressing this issue in validators.
Currently you must disable output escaping in settings.yml, otherwise Apostrophe slots will not work



# Output escaping settings

escaping_strategy: false


(We are looking into ways to support either setting so that Apostrophe does not force you to choose
the same strategy for your own Symfony code. If we can do that without too much overhead, we

Now, load the fixtures for a basic site. Every site begins with a home

page with all other pages being added as descendants of the home page:


21 | P a g e
./symfony doctrine:build --all

./symfony doctrine:data-load


Note: if you are adding apostrophePlugin to an existin site you probably don't want to delete your
other tables and start over! You can do:


./symfony doctrine:build --all-classes --sql


To build the model classes and to build the SQL code that would be sufficient to recreate your
database. Then look in data/sql/schema.sql for the CREATE TABLE statements for tables beginning
with the `a_` prefix.

Ideally you would use Doctrine's migration schema diff task instead. Unfortunately there is currently
a bug in Symfony and Doctrine that prevents successful use of this feature with tables that use
inheritance. See [http://trac.symfony-project.org/ticket/7272 Symfony bug 7272 ] for more
information and patches if you are interested in pursuing that approach. It's probably easier to just
locate the CREATE TABLE statements.

You will also want to load the fixtures for the apostrophePlugin or, depending on your needs, edit
them to suit your purposes better first. You can do so without clobbering your existing database:


./symfony doctrine:data-load --append plugins/apostrophePlugin/data/fixtures/seed_data.yml


In particular you must have a home page and the hidden `/admin` and `/admin/media` engine pages.

22 | P a g e
Also see the fixtures provided with the sandbox project for strongly recommended (but not
mandatory) permissions and groups settings.

==== .htaccess Rules For Media ====

The media module of Apostrophe uses carefully designed URLs to allow images to be

served as static files after they are generated for the first time. This

is done at the Apache level to maximize performance: PHP (and therefore

Symfony) don't have to get involved at all after the first time an image

is rendered at a particular size.

The following special .htaccess rules are required to enable this. These

should be copied to your .htaccess file after the

`RewriteBase /` rule, if you are using one, but before any other rules.


###### BEGIN special handling for the media module's cached scaled images

# If it exists, just deliver it

RewriteCond %{REQUEST_URI} ^/uploads/media_items/.+$

RewriteCond %{REQUEST_FILENAME} -f

RewriteRule .* - [L]

# If it doesn't exist, render it via the front end controller

RewriteCond %{REQUEST_URI} ^/uploads/media_items/.+$

RewriteCond %{REQUEST_FILENAME} !-f

RewriteRule ^(.*)$ index.php [QSA,L]

###### END special handling for the media module's cached scaled images


==== Routing Rules ====

23 | P a g e
By default Apostrophe will map CMS pages to URLs beginning with `/cms`:




And leave all other URLs alone. This is appropriate if the CMS is a minor part of your site. If the CMS
is the main purpose of your site, shut off the automatic registration of the route above in app.yml:



routes_register: false


And register these as the LAST rules in your application's `routing.yml` file instead:


# A default rule that gets us to actions outside of the CMS.

# Note that you can't have regular CMS pages with a slug beginning with /admin

# on an Apostrophe site. At some point we'll make this reserved prefix more

# configurable.




url: /admin/:module/:action/*


24 | P a g e

# A homepage rule is expected by a and various other plugins,

# so be sure to have one




url: /

param: { module: a, action: show, slug: / }



# Put any routing rules for other modules and actions HERE,

# before the catch-all rule that routes URLs to the

# CMS by default.



# Must be the last rule




url: /:slug

param: { module: a, action: show }

requirements: { slug: .* }


25 | P a g e
Thanks to Stephen Ostrow for his help with these rules.

'''You can change the a_page rule, prefixing the page slug with anything you wish, but you must have
such a rule and it must be named `a_page`. Otherwise Apostrophe engines, including the built-in
media repository functionality and media slots, will not work.'''

==== layout.php Requirements ====

Apostrophe's non-CMS pages, such as the "reorganize" feature, the user admin feature and the
media repository, expect a layout that offers certain overridable Symfony slots, in particular the `a-
subnav` slot. The media repository overrides this slot to offer media navigation links instead of a list
of subpages. If your application layout does not offer such a slot to override, the media repository
will be missing some of its interface.

Take a close look at `modules/a/templates/layout.php` and check out the way we've provided the
ability for page templates to override various parts of the layout via Symfony slots. If you experience
missing bits of the UI with your own layout, make sure you offer the same slots at appropriate

** Prior to Apostrophe 1.0.7 ** the media repository in particular defaults to using a built-in layout
that is not really suitable for its current markup. You can fix this with an `app.yml` setting which is
also found in the sandbox project:




use_bundled_layout: false


Beginning in Apostrophe 1.0.7 this is no longer necessary.

26 | P a g e
==== Styling Requirements ====

The user interface will be essentially complete at this point, but you may notice a few areas in the
media repository where the look and feel is a bit too austere unless you borrow elements from the
sandbox project's `main.css` file or apply some styles of your own. In fact in Apostrophe 1.1 we will
be migrating toward moving more of the hard-to-override, site-design-specific CSS from `a.css` to
the `main.css` file of the sandbox project. So you may want to refer to that file as a reference.

==== Set Permissions ====

In Symfony, the `cache` and `web/uploads` folders are always world-writable so that the web server
can store files. Apostrophe adds one more such folder, `data/a_writable`. This folder is used for files
that should be writable by the web server but not directly accessible via a URL. This folder is used for
search indexes and other data that does not live in the database.

You can set up this folder yourself, but it's more convenient to use Symfony's `project:permissions`
task, which apostrophePlugin enhances to handle this folder:


./symfony project:permissions


==== Performance Optimization ====

Sites with many pages sometimes require some tuning for good performance.

By default, Apostrophe is configured to provide good navigation and reasonable performance

without running out of memory, even if your site has thousands of pages. Previously a separate
setting was provided to specify that your site has many pages, but this setting has been removed as
we've found the code for the "worst case" scenario is acceptably fast on small sites too.

27 | P a g e
However, there is an issue with searches on sites with thousands of pages. Searches for common
words can exhaust the memory available.

You can address this by setting a hard limit on the number of pages Apostrophe should consider
when generating search results. Note that this is not the same thing as the number of results the
user will see, because some results may be locked pages the user is not eligible to see. That filtering
takes place after the hard limit, otherwise it does not save memory. So the limit should be set

To set a hard limit on the memory required for a search, set this `app.yml` option:




search_hard_limit: 1000


1000 is a good choice because it will not exhaust a realistically chosen PHP `memory_limit` and
almost certainly will not exclude any worthwhile results either.

==== Clear the Cache and Get to Work! ====

No Symfony developer will be shocked to hear that you need to clear your Symfony cache at this


./symfony cc


==== Scheduled Tasks: Enabling Linked Accounts ====

28 | P a g e
'''Version 1.5 and above:''' if you wish to take advantage of the "Linked Accounts" feature, which
automatically brings new media into the media repository from Youtube and other media services
specified by the site administrator, you will need to schedule the following command line task to run
on a scheduled basis. We suggest using a cron job to run this task every 20 minutes. Change the `--
env` setting as appropriate to your environment:


./symfony apostrophe:update-linked-accounts --env=prod


==== Conclusion ====

We're done installing Apostrophe! On to the [ManualEditorsGuide editor's guide], which briefly
explains how to edit content with Apostrophe- briefly because it is so much more fun to simply use

[ManualEditorsGuide Continue to the Editor's Guide]

[ManualOverview Up to the Overview]

29 | P a g e
= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Upgrading Apostrophe ==

=== Overview ===

From time to time we issue upgrades for Apostrophe, more specifically for the open source Symfony
plugins `apostrophePlugin` and `apostropheBlogPlugin`. For the more part this process is all but
automatic within the Apostrophe 1.x series. However, sometimes a major change does justify a few
manual steps to allow new features to be implemented. We'll add notes on these issues to this page
over time to ensure that an upgrade path is available, just as we did during the transition from
`pkContextCMS` to `apostrophePlugin`.

=== Standard Upgrade Steps ===

* If you are installing Apostrophe via the `plugin:install` task or downloading Symfony plugin tarballs
directly from the Symfony plugins site, then of course you'll need to download the new version. If
you used `plugin:install` you should be able to use `plugin:upgrade` to install the latest version of
`apostrophePlugin`. Remember that you should upgrade the blog plugin at the same time.

* Always run the `symfony cc` task after upgrading.

* Always run the `apostrophe:migrate` task after upgrading. For instance, on your development
computer, you would type:

`./symfony apostrophe:migrate --env=dev`

This task upgrades database tables as needed. If the blog plugin is present it will also upgrade its
database tables. For the most part we avoid unnecessary schema changes but sometimes additions
are necessary to deliver new features. We generally avoid these in patchlevel releases (1.4.2,

30 | P a g e
1.4.3...) but they may be necessary to address bugs in rare cases. They are more common in minor
version changes (1.4, 1.5).

If you are not using MySQL, then the `apostrophe:migrate` task will not work directly for you.
However the source code of the task remains a valuable resource for determining the necessary

=== Version-Specific Upgrade Notes ===

Be sure to consult these when migrating from an older version up to or beyond the version

==== Version 1.5 ====

'''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 beginning in version 1.5.

Developers: if you have created custom slot types, either on your own or by using the
`apostrophe:generate-slot-type` task, you will need to make a small change to the `normalView`
partial of your slot.

Beginning in version 1.5, the `slot` parameter must be added to the list of parameters to the
`simpleEditButton` partial. For example:


<?php include_partial('a/simpleEditButton', array('pageid' => $page->id, 'name' => $name, 'permid'

=> $permid, 'slot' => $slot)) ?>


31 | P a g e
If you are using `simpleEditWithVariants` you won't need to change anything. If you are not using a
standard edit view at all (many slots, such as our slideshow slot, do not need one), then you don't
have to worry about this change.

32 | P a g e
= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Editor's Guide ==

This section is devoted to users who will participate in editing, maintaining and administering the
content of the site. All of these tasks are performed through your web browser. No command line
skills are required.

Access your new Apostrophe site's URL to see the home page.

Click "log in" and log in as the admin user (username admin, password demo)

to see the editing controls. Notice that editing controls are added to the normal experience of the
site. This greatly reduces the learning curve for editors.

=== Managing Pages ===

When a user has appropriate privileges on a page, they are able to make

the following changes by clicking the "This Page" button, then interacting with a simple "breadcrumb
trail" that appears at the top of the page:

* Rename the page by clicking on the page title

* Add a child page beneath the current page

* Open the page management settings dialog via "This Page," then the "gear" icon for less frequent

Apostrophe emphasizes "unpublishing" pages as the preferred way

of "almost" deleting them because it is not permanent. Anonymous users, and users who do not

have editing privileges on the page, will see the usual 404 Not Found error.

But users with suitable editing privileges will see the page with its title

33 | P a g e
"struck through" and will be able to undelete the page if they desire. This prevents the loss of

You can also delete a page permanently via the small X in the lower right

corner of the page settings dialog. Most of the time that's a shortsighted

thing to do, but it is useful when you create an unnecessary page by accident.

The side navigation column also offers an editing tool: users with

editing privileges can change the order of child pages listed there

by dragging and dropping them. (If a page has no children, the side

navigation displays its peers instead, including itself.) You can do the same thing with the tabs at the
top of the page. Also check out the "Reorganize" button, which is discussed in more detail later.

=== Editing Slots ===

What about the actual content of the page? The editable content

of a page is stored in "slots" (_note to developers: not the same thing as Symfony slots_).

CMS slots can be of several types:

* Plaintext slots (single line or multiline)

* Rich text slots (edited via FCK)

* Feed slots (RSS or Atom feeds, such as Twitter feeds, inserted into a page)

* Raw HTML slots (best avoided in good designs, but useful when you must paste raw embed codes)

* Media slots (image, slideshow, video, PDF, button)

* Custom slots (of any type, implemented as described in the developer's guide section)

Once you have logged in, you'll note that each editable slot has an "Edit" button or, in the case of
the media slots, "Select Image" and similar buttons.

34 | P a g e
Every slot also offers version control. The arrow-in-a-circle icon

accesses a dropdown list of all changes that have been made to that slot,

labeled by date, time and author and including a short summary of the change

to help you remember what's different about that version. Pick any version to preview it. Click "Save
as current version" to permanently revert to that version.

=== Editing Areas ===

In addition to single slots, apostrophePlugin also supports

"areas." Areas are vertical columns containing more than one slot.

Editing users are able to add and remove slots from an area at any time,

selecting from a list of slots approved for use in that area. The slots

can also be reordered via up and down arrow buttons (used here instead

of drag and drop to avoid possible browser bugs when dragging and dropping

complex HTML, and because drag and drop is not actually much fun to use

when a column spans multiple pages).

The usefulness of areas becomes clear when rich text slots are interleaved with media slots. Media
slots provide a robust way to manage photos and videos without wrecking the layout of the site. You
can upload photos and select and embed videos freely without worrying about their size and format.

=== Revising History ===

Did you make a mistake? Not a problem! Both slots and areas allow you to roll back to any previous
edit. Just click "History," preview versions by clicking on them, and click "Save As Current Revision"
when you find the version you want.

=== Editing Media ===

Click "Add Slot," then "Image," then "Select Image." You will be taken to the media area of the site.

Here you can select any of the images that have already been uploaded to the site, or upload new
images. This media repository provides a clean way to keep track of the media you have available.

35 | P a g e
You can do much the same thing with video. YouTube is tightly integrated with Apostrophe, and you
can easily search YouTube from within the media interface. You can also paste embed codes from
other sites and add thumbnail images for those videos manually.

You can organize media with categories and tags. Add new categories using the "Manage
Categories" button at the left, in the media browser area.

It's possible to add a media page to the public-facing part of the site that displays only media in
certain categories. To do that, go to the home page, add a new page and give it a title that relates to
a media category. When the new page appears, click "This Page," then the gear icon. When the page
settings dialog appears, select "Media" from the "Page Engine" menu.

Once you select Media, a category selector will appear. Pick the appropriate category, then click
Save. Note that you can select more than one category.

The page will now refresh and display a media browser that anyone on your site can use to see
media in that category, most recent first, with all of the browsing features that are standard in the
media area.

=== Reorganizing the Site ===

Apostrophe offers drag-and-drop reordering of the children of

any page via the navigation links on the left-hand side. You can also reorder the tabs at the top of
any page by dragging and dropping.

However, there are times when you want to do something less linear, like moving a page up or down
in the page tree.

To do that, click on the "Reorganize" button at the top of any page. This will take you to the
reorganize tool, a page where you can drag and drop pages to any position in the page tree for quick
and painless reorganization of the site.

This tool is only available to site administrators such as the `admin` user.

36 | P a g e
[ManualDesignersGuide Continue to the Designer's Guide]

[ManualOverview Up to the Overview]

37 | P a g e
= Apostrophe Manual =

[ManualOverview Up to Overview]

== Designer's Guide ==

This section explains how to go about customizing the appearance and behavior of the site without
writing new code (apart from simple calls to insert slots and otherwise use PHP as a templating
language). It is intended to be read by front end designers (also known as front end developers).

Some familiarity with Symfony is expected. You will need to edit files like
`apps/frontend/config/app.yml` to adjust settings, and you will be creating `.php` files although full
PHP programming skills are not required.

=== Title Prefix ===

By default, the title element of each page will contain the title of that page.

In many cases you'll wish to specify a prefix for the title as well.

You can do so by setting `app_a_title_prefix` in `app.yml`. This option supports

optional internationalization:




# You can do it this way...


en: 'Our Company : '

fr: 'French Prefix : '

# OR this way for a single-culture site

title_prefix: 'Our Company'

38 | P a g e

Note that all changes to `app.yml` should be followed by a `symfony cc` command:


./symfony cc


=== Creating and Managing Page Templates and Layouts ===

Where do slots appear in a page? And how do you insert them?

Slots can be inserted in two places: in your site's <tt>layout.php</tt>

file, which decorates all pages, and in page template files, which can

be assigned to individual pages.

=== How to Customize the Layout ===

By default, the CMS will use the

<tt>layout.php</tt> file bundled with it. If you wish, you can turn this

off via app.yml:




use_bundled_layout: false


CMS pages will then use your application's default layout. One strategy

39 | P a g e
is to copy our <tt>layout.php</tt> to your application's template folder

and customize it there after turning off use_bundled_layout.

=== How to Customize the Page Templates ===

The layout is a good place for global elements that should appear on

every page. But elements specific to certain types of pages are better

kept in page templates. These are standard Symfony template files with

a special naming convention.

Page template files live in the templates folder of the a module.

We provide these templates "out of the box:"

* homeTemplate.php

* defaultTemplate.php

homeTemplate.php is used by our default home page, and defaultTemplate.php

is the default template if no other template is chosen.

You can change the template used by a page by using the

template dropdown in the breadcrumb trail. This does not delete

the slots used by the previous template, so you can switch back

without losing your work.

How do you create your own template files? ''Don't'' alter the templates

folder of the plugin. As always with Symfony modules, you should

instead create your own a/templates folder within your

application's modules folder:

40 | P a g e

mkdir -p apps/frontend/modules/a/templates


Now you can copy homeTemplate.php and defaultTemplate.php to this folder,

or just start over from scratch. You can also copy _login.php if you don't

like the way we present the login and logout options. We ''do not recommend'' altering the rest of
the templates unless

you have a clear understanding of their purpose and function and are

willing to make ongoing changes when new releases are made. In general,

if you can use CSS to match the behavior of our HTML to your needs,

that will be more forwards-compatible with new releases of the CMS.

If you add additional template files, you'll need to adjust

the `app_a_templates` setting in `app.yml` so that your

new templates also appear in the dropdown menu:






Home Page


Default Page


My Template

41 | P a g e

=== Custom Navigation in Templates and Layouts ===

Apostrophe supports three types of navigation components. These can be found in the aNavigation
module. Including these navigation elements is as easy as including a component in your template
or in `layout.php`. The three types of navigation elements are:

* An accordion tree

* A tabbed or vertical menu of links to child pages

* A breadcrumb trail from the home page to the current page.

The navigation element, once used, will no longer require any additional SQL queries no matter how
many similar navigation elements are included in a template or layout.

==== Accordion Navigation ====

Accordion-style navigation can be easily included in your template by doing the following. Accordion
navigation is a compromise between showing the entire page tree and showing just the breadcrumb.
Accordion navigation includes everything that would be included in a breadcrumb trail, plus the
peers of all of those pages. This makes it easy to navigate to related pages at any level without being
overwhelmed by a comprehensive list of all pages on the site. These links are rendered as nested `ul`


<?php include_component('aNavigation', 'accordion', array('root' => '/', 'active' => $page->slug,

'name' => 'normal')) ?>



* root (required) - This is the slug of the root page that the menu should start from. Often the home

42 | P a g e
* active (required) - The page to build the navigation down to, will also recieve a css class of current

* name (required) - The unique name of the navigation element for your template. This influences
CSS IDs and can be used for styling

==== Tabbed Navigation ====

The tabbed navigation provides a navigation element that displays all of the children of a page. Note
that depending on your CSS you can render it as a vertical menu. For an example, see the "subnav"
area of the `asandbox` project; see `a/templates/_subnav.php).


<?php include_component('aNavigation', 'tabs', array('root' => '/', 'active' => $page->slug, 'name'
=> 'tabs')) ?>



* root (required) - This is the parent slug of the tabs that are displayed.

* active (required) - This is the page that if in the navigation will recieve a current css class of current

* name (required) - The unique name of the navigation element for your template

* extras (optional) - Additional links to present at the beginning of the list

You can also inject additional links at the beginning of the list:


<?php include_component('aNavigation', 'tabs', array('root' => '/', 'active' => $page->slug, 'name'
=> 'tabs', 'extras' => array('/page/slug/or/absolute/url' => 'Label'))) ?>


If you wish to link to a Symfony action on the site as an additional tab, you'll need to use `url_for` to
create an absolute URL to it:

43 | P a g e

'extras' => array(url_for('module/action?my=parameter', true) => 'Label')


==== Breadcrumb ====


<?php include_component('aNavigation', 'breadcrumb', array('root' => '/', 'active' => $page->slug,

'name' => 'bread')) ?>



* root (required) - This is the root slug of the page the breadcrumb should begin with. Usually the
home page

* active (required) - This is the last element that the navigation should descend to. Usually the
current page

* separator (optional) - The separator to use between items, defaults to " > "

* name (required) - the unique name of the navigation element for your template.

=== Inserting Slots in Layouts and Templates === #slots

Of course, creating layouts and templates does you little good if you

can't insert user-edited content into them. This is where the

CMS slot helpers come in.

Here's how to insert a slot into a layout or page template:


<?php # Once at the top of the file ?>

44 | P a g e
<?php use_helper('a') ?>



<?php # Anywhere you want a particular slot ?>

<?php a_slot('body', 'aRichText') ?>


Notice that two arguments are passed to the <tt>a_slot</tt>

helper. The first argument is the name of the slot, which distinguishes

it from other slots on the same page. *Slot names should contain only

characters that are allowed in HTML ID and NAME attributes*. We recommend

that you use only letters, digits, underscores and dashes in slot names.

The slot name will never be seen by the user. It is a useful label

such as `body` or `sidebar` or `subtitle`.

The second argument is the type of the slot. "Out of the box,"

apostrophePlugin offers a useful array of slot types:

* aText (plaintext)

* aRichText (allows WYSIWYG formatting)

* aFeed (brings any RSS or Atom feed into the page)

* aImage (still images from the media repository)

* aSlideshow (a series of images from the media repository)

* aButton (an image from the media repository, and an editor-configurable link)

* aVideo (video from !YouTube and other providers)

* aPDF (PDF documents from the media repository)

45 | P a g e
* aRawHTML (Unfiltered HTML code)

The use of these slots is largely self-explanatory and we encourage you to play with the demo and
try them out.

You can add additional slot types of your own and release and

distribute them as plugins as explained in the developers' section in this document.

==== aText Slots ====

aText slots contain only plaintext (actually, valid HTML text, which can include valid HTML and UTF-8
entities). URLs are automatically rendered as links, email addresses are rendered as obfuscated
`mailto:` links, and newlines are rendered as line breaks.

Note that the special slot name `title` is reserved for the title of the page

and is always of the type `aText`. While you don't really need to provide an additional editing
interface for the title, you might also want to insert it elsewhere in your

page layout or template as a design element:


<?php a_slot('title', 'aText') ?>


The behavior of most slot types can be influenced by passing options to them from the template or
layout. You do this by passing an array of options as a third argument

to the helper, like this:


<?php a_slot('aboutus', 'aText', array('multiline' => true)) ?>


46 | P a g e
The `multiline` option specifies that a plaintext slot should permit multiple-line text input.

==== aRichText Slots ====

Here is a simple example of a rich text slot:


<?php a_slot('ourproducts', 'aRichText') ?>


The rich text slot allows users to edit content using a rich text editor. The HTML they enter is filtered
for correctness and to ensure it does not damage your design. Read on for more information about
ways to adjust these filters.

The `tool` option is one of the most useful options for use with rich text slots:


<?php a_slot('subtitle', 'aRichText',

array('tool' => 'basic')) ?>


Here we create a subtitle rich text slot on the page which is editable, but only

with the limited palette of options provided in FCK's `basic` toolbar (bold, italic and links).

Note that you can create your own custom toolbars for the FCK rich text editor. Add these to
`web/js/fckextraconfig.js` (at the project level, not in the plugin) and they will be found
automatically. Here is an example:


47 | P a g e
FCKConfig.ToolbarSets["Sidebar"] = [




For more complete examples of what can be included here, see

`apostrophePlugin/web/js/fckeditor/fckconfig.js`. (You do not need to, and should not, duplicate this
entire file in `fckextraconfig.js`. Just add and override things as needed.)

Other notable options to `aRichText` slots include:

* allowed-tags is a list of HTML elements to be permitted inside the rich text. By default Apostrophe
filters out most HTML elements to prevent pasted content from Microsoft Word and the like from
wrecking page layouts. You can pass an array of HTML element names (without angle brackets), or a
string like that accepted by the PHP `strip_tags` function.

This allows us to write a better version of the `subtitle` slot above that filters the HTML to make sure
it's suitable to appear in a particular context:

<?php a_slot('subtitle', 'aRichText',

array('tool' => 'basic', 'allowed-tags' => '<b><i><strong><em><a>')) ?>

Note that we list both the `b` tag and the `strong` tag here. We do this because different browsers
submit slightly different rich text.

The default list of allowed tags is:

`h3, h4, h5, h6, blockquote, p, a, ul, ol, nl, li, b, i, strong, em, strike, code, hr, br, div, table, thead,
caption, tbody, tr, th, td, pre`

48 | P a g e
* allowed-attributes is a list of HTML attributes to be accepted, on a per-element basis. Unlike
`strip_tags` Apostrophe's robust, high-performance HTML filter, `aHtml::simplify`, removes all
inappropriate HTML attributes. Here is the default list of allowed attributes:



"a" => array("href", "name", "target"),

"img" => array("src")



* allowed-styles is a list of CSS style names to be permitted, again on a per-element basis. By

default, no styles are permitted in rich text editor content. You can alter this to allow more creative
table styling, at the cost that a truly persistent user might manage to wreck the page layout. The
format is the same as that used above for allowed attributes.

If you want to change the default settings for allowed tags, attributes and styles, you may find it
more convenient to use `app.yml` for this purpose (default values are shown, '''be sure to keep
them''' unless you specifically want to disable things that are normally considered essential):




allowed_tags: [h3, h4, h5, h6, blockquote, p, a, ul, ol, nl, li, b, i, strong, em, strike, code, hr, br, div,
table, thead, caption, tbody, tr, th, td, pre]


a: [ href, name, target ]

img: [ src ]

allowed_styles: ~


49 | P a g e
==== aFeed Slots ====

The aFeed slot allows editors to insert an RSS feed into the page:


<?php a_slot('subtitle', 'aFeed') ?>


When the user clicks "Edit," they are invited to paste an RSS feed URL. When they click "Save," the
five most recent posts in that feed appear. The title of each feed post links to the original article on
the site of origin as specified by the feed.

You can change this behavior with the following options. The defaults are shown as examples:

* `'links' => true` determines whether links to the original posts are provided.

* `'dateFormat' => 'Y m d'` lets you specify your own date and time format (see the PHP date()
function). 'Y m d' is not actually our default, as our default is more subtle than can be output with
the date function alone (we include the year only when necessary and so on). Our default is very
nice, but very American, so feel free to override it.

* `'markup' => '<strong><em><p><br><ul><li>'` changes the list of HTML elements we let through in
markup. We apply our usual aHtml::simplify() method, which is smarter than PHP's `strip_tags`

* `'interval' => 300` specifies how long we hold on to a feed before we fetch it again. This is
important to avoid slowing down your site or getting banned by feed hosts. We rock Symfony's
caching classes to implement this. Check out aFeed::getCachedFeed if you're into that sort of thing.

* `'posts' => 5` determines how many posts are shown.

==== aImage Slots ====

Image slots are used to insert single still images from Apostrophe's built-in media repository:


<?php a_slot('landscape', 'aImage') ?>

50 | P a g e

Options are available to control many aspects of image slots:

* `'width' => 440` determines the width of the image, in pixels. Images are never rendered larger
than actual size as upsampling always looks awful.

* `'height' => 330` determines the height of the image, in pixels. It is ignored if `flexHeight' => true is
also present.

* `'resizeType' => 's'` determines the scaling and cropping style. The `s` style scales the image down
if needed but never changes the aspect ratio, so the image is surrounded with white bars if
necessary (see `flexHeight` for a way to avoid this). The `c` style crops the largest part of the center
of the image that matches the requested aspect ratio.

* `'flexHeight' => false` determines whether the height of the image is scaled along with the
requested width to maintain the aspect ratio of the original. Set this option to `true` to scale the

* `'defaultImage' => false` allows you to specify the URL of an image to be displayed if no image has
been selected by the editor yet. Otherwise no image is displayed until an editor chooses one.

* `'title' => false` specifies whether the title associated with the image in the media repository
should be shown on the page. CSS can be used to display this title in a variety of interesting ways.

* `'description' => false` specifies whether the rich text description associated with the image in the
media repository should be shown on the page.

* `'link' => false` specifies a URL that the image should link to when clicked upon. Note that in most
cases you probably want to use an aButton slot for this sort of thing.

These options suffice for most purposes. There is one more option, `constraints`, which takes an
array of constraints that can be used to limit which images the user can select from the media
repository. By default no constraints are applied. The possible constraints are:

* 'aspect-width' and 'aspect-height' specify an aspect ratio. The media browser will show only media
that match this aspect ratio. Both must be specified. For instance:


'constraints' => array('aspect-width' => 4, 'aspect-height' => 3)

51 | P a g e

* 'minimum-width' and 'minimum-height' specify minimum width and height in pixels. Only media
meeting these minimum criteria will be shown. You are not required to use both options although it
usually makes sense to specify both.

* 'width' and 'height' specify that only images of that '''exact''' width and/or height should be
selectable for this image slot. It is not mandatory to specify both, and if you are using `flexHeight`
you might not want to specify a height constraint.

Here are three examples:

This slot will render the image 400 pixels wide. The original must be at least 400 pixels wide. The
height will scale along with the width.


<?php a_slot('landscape', 'aImage', array('width' => 400, 'flexHeight' => true, 'constraints' =>
array('minimum-width' => 400))) ?>


This slot will be render images at 640x480 pixels. The original must be at least 640 pixels wide. The
aspect ratio must be 4x3.


<?php a_slot('landscape', 'aImage', array('width' => 640, 'height' => 480, 'constraints' =>
array('minimum-width' => 640, 'aspect-width' => 4, 'aspect-height' => 3))) ?>


This slot will crop the largest portion of the center of the selected image with a 400x200 aspect ratio
and scale it to 400x200 pixels. The selected image must be at least 400 pixels wide.

52 | P a g e

<?php a_slot('landscape', 'aImage', array('width' => 400, 'height' => 200, 'resizeType' => 'c',
'constraints' => array('minimum-width' => 400))) ?>


==== aSlideshow Slots ====

Slideshow slots allow editors to select one or more images and display them as a slideshow. The
exact behavior of the slideshow can be controlled by various options.

Here is a simple example:


<?php a_slot('travel', 'aSlideshow') ?>


By default users can select any images they wish and add them to the slideshow, and the slideshow
will advance when they click on an image. Left and right arrows to move back and forward in the
slideshow appear above the image unless turned off by the `arrows` option.

Slideshows with images of varying dimensions can be confusing to look at unless the aspect ratio is
the same. Fortunately slideshows support all of the sizing, cropping and constraint options that are
available for the aImage slot (see above). We recommend either using `resizeType => 'c'` to crop to a
consistent size (and therefore aspect ratio) or using the `aspect-width` and `aspect-height`
parameters to the `constraints` option to ensure a consistent aspect ratio.

'''In addition to all of the options supported by the aImage slot''' (see above), slideshow slots support
the following additional options:

* `'random' => false` determines whether the slideshow is shown in the order selected by the editor
or in a random order. Set this option to `true` for a random order.

53 | P a g e
* `'arrows' => true` indicates that arrows to move forward and backward in the slideshow should
appear above the image. By default they do appear. You can turn this off by passing `false`.

* `'interval' => false` sets the interval for automatic advance to the next image in the slideshow. By
default the slideshow does not advance automatically. For automatic advance every ten seconds
pass `'interval' => 10`. Slideshows automatically repeat once they have advanced to the final image.
If the user clicks on the slideshow, auto-advance stops, and the user can navigate manually by
clicking to advance or using the arrows if present.

* `'credit' => false` determines whether the credit field from the media repository is shown with
each image.

* `'itemTemplate' => slideshowItem` allows you to specify a different slideshow item template at
the project level. The default template is bundled with the slot and is named `_slideshowItem.php`.

==== aButton Slots ====

Button slots are similar to image slots. However, they also allow the editor to specify a target URL
and a plaintext title. When they click on the button, they are taken to the URL.

Here is an example:


<?php a_slot('logo', 'aButton') ?>


Button slots support all of the options supported by the aImage slot (see above). In particular you
might want to specify a default image.

If the title and/or description options are turned on, they are rendered as links to the button's
destination URL.

54 | P a g e
==== aVideo Slots ====

The aVideo slot allows video to be embedded in a page.

Apostrophe's media repository does not store video directly. Instead Apostrophe manages videos
hosted on external services such as !YouTube, Viddler and Vimeo. !YouTube is tightly integrated into
Apostrophe, so editors can search !YouTube directly from the repository in order to easily add
videos. Embed codes for other video services can also be added to the media repository. Either way,
Apostrophe wrangles the necessary embed codes and ensures that they render at the desired size.

Here is an example:


<?php a_slot('screencast', 'aVideo') ?>


Note that you can disable support for embed codes from non-!YouTube services if you wish:




embed_codes: false


Note that the plugin itself ships with this feature disabled by default, but the `app.yml` file of our
sandbox project does turn it on.

''' The video slot supports all of the options supported by the image slot''' (described above), with
the following differences and exceptions:

55 | P a g e
* The `width` option defaults to 320 pixels.

* The `height` option defaults to 240 pixels.

* The `resizeType` option is ignored. As a general rule video hosting services do not permit cropping
and choose their own approaches to letterboxing. We recommend using the `flexHeight` option if
there is any doubt about the dimensions of the video.

* The `constraints` option is supported. The constraints will be applied based on the dimensions of
the original video (if from !YouTube). For videos from other hosting services (pasted as embed codes
in the media repository) constraints are based on the dimensions of the thumbnail manually
uploaded by the editor.

* The `link` and `defaultImage` options are not supported.

==== aPDF Slots ====

The aPDF slot allows PDF documents to be embedded in a page. If ghostscript and netpbm are
installed on the server, thumbnail previews of the first page are automatically displayed in PDF slots,
and clicking on them launches the PDF. If they are not available, a clickable PDF icon is still displayed
as a way of launching the PDF.

aPDF slots support the following options, which behave as they do for the aImage slot (described

* The `width` option defaults to 170 pixels.

* The `height` option defaults to 220 pixels.

* The `flexHeight` option is available to scale the thumbnail based on the page size, although this is
only relevant if netpbm and ghostscript are available.

* The `defaultImage` option is available. The default image appears when no PDF has been selected

* The `constraints` option is available, and can be used to select only PDFs with a certain aspect
ratio, such as 8.5x11 (use the aspect-width and aspect-height parameters). This option should not be
used when netpbm and ghostscript are not available as the true size of the PDF is not known in this

==== aRawHTML Slots: When You Must Have Raw HTML ====

56 | P a g e
Honestly, we're not big fans of raw HTML slots. Editors tend to paste code that breaks page layouts
on a fairly regular basis. That's why our rich text slots filter it so carefully.

However there are times when you must have the embed code for a mailing list signup form service
or similar third-party website feature.

In these situations the aRawHTML slot is useful.

Here is an example:


<?php a_slot('mailinglist', 'aRawHTML') ?>


This slot displays a multiline text entry form where the editor can paste in HTML code directly. This
code is not validated in any way, so you should think carefully before offering this feature to less
experienced editors.

There is a way to recover if you paste bad HTML that breaks the page layout. Visit the page with the
following appended to the URL:




When this parameter is present in the URL, raw HTML slots will escape their contents so that you see
the source code, safely defanged. You can then click "Edit" and make corrections as needed.

That's it for the standard slots included with Apostrophe! Of course there will be more. And you can
add more yourself. See the developer's section of this document.

57 | P a g e
=== Slot Variants: More Mileage From Your Templates === #variants

It's possible to specify different options to each slot in different page templates. And for many
designs this is the way to go.

But sometimes you'd like to have a more flexible setup in which the same template or layout can
serve the needs of more pages.

One solution would be to let users specify slot options or even CSS classes directly, but this has a
tendency to be confusing. So we've provided a better solution: slot variants.

If you define variants like this in `app.yml`:





label: Normal


interval: 0

title: false

arrows: true


label: Compact


interval: 0

title: true

arrows: true

itemTemplate: slideshowItemCompact


58 | P a g e
label: Auto Play


interval: 4

title: true

arrows: false

itemTemplate: slideshowItemCompact


And you have allowed these variants, either by not specifying the `app_a_allowed_slot_variants`
option at all (note that it is set but empty in our sandbox project) or by setting it up as follows:




- normal

- compact

- autoplay


Then the user will be presented with a choice of these three variations on the slideshow slot in an
"Options" menu to the right of the "Choose Images" button, provided that we have actually allowed
those variants.

How does this work? Each variant has a label and an optional set of options. When the user switches
between variants, the slot is refreshed with the new options in effect.

Sometimes you will want to define variants on a slot that are only suitable for use in a particular
place. For instance, you wouldn't want to allow an "extra-wide" image slot in a sidebar. Beginning in
version 1.04, you can address this by passing the `allowed_variants` option when inserting an area or

59 | P a g e
<?php a_slot('aImage', array('allowed_variants' => array('narrow'))) ?>

Note that this means that you can lock a slot down to a single variant that would not otherwise be
the default. If you allow only one variant, the options menu does not appear, since there are no
other choices.

You can do the same thing for an area by adding `allowed_variants` to the `type_options` for the slot
type in question.

'''Prior to version 1.4.1/svn commit 1569 the `app_a_allowed_slot_variants` option did not exist''', so
it was necessary to specifically allow only the appropriate variants in every `a_slot` or `a_area` call if
any of the variants were template-specific and a poor choice for general use. This new option makes
it easy to set up a more restrictive list of variants that are allowed when `allowed_variants` is not
specified at the slot level.

'''In addition to changing options, variants also set a CSS class''' on the slot's outermost container.
Specifically, when the `compact` option is in use, the CSS class `compact` will be set on that slot's
outermost container. This is very useful for styling purposes.

'''Prior to version 1.04 slot variant options must fully contradict each other''' for consistent results.
That is, if you set the `interval` option for one variant, you must set a value for it for every variant
(even if that value is false). This requirement is removed in version 1.04.

What about newly added slots? If there are variants for a slot, the first variant in the list is used for
new slots of that type (in areas) or for slots whose variant has never been set (for standalone slots).
If an `allowed_variants` option is present for a particular slot or area, or there is a global
`app_a_allowed_slot_variants` option, then the first allowed variant is used.

=== Inserting Areas: Unlimited Slots in a Vertical Column === #areas

Slots are great on their own. But when you want to mix paragraphs of text with

elements inserted by custom slots, it is necessary to create a separate

60 | P a g e
template file for every page. This is tedious and requires the

involvement of an HTML-savvy person on a regular basis.

Fortunately apostrophePlugin also offers "areas." An

area is a continuous vertical column containing multiple slots which

can be managed on the fly without the need for template changes.

You insert an area by calling a_area($name) rather than



<?php a_area("sidebar") ?>


When you insert an area you are presented with a slightly different

editing interface. At first there are no editable slots in the area.

Click "Insert Slot" to add the first one. You can now edit that

first slot and save it.

Add more slots and you'll find that you are also able to delete them

and reorder them at will.

By default new slots appear at the top of an area. If you don't like this,

you can change it for your entire site via `app.yml`:



61 | P a g e

new_slots_top: false


An area has just one version control button for the entire area. This

is because creating, deleting, and reordering slots are themselves

actions that can be undone through version control.

In a project with many custom slot types, you may find it is

inappropriate to use certain slot types in certain areas. You can

specify a list of allowed slot types like this:


<?php a_area("sidebar",

array("allowed_types" => array("aText", "myCustomType"))) ?>


Notice that the second argument to `a_area` is an

associative array of options. The `allowed_types` option allows us to

specify a list of slot types that are allowed in this particular area.

In addition, you can pass options to the slots of each type, much as

you would when inserting a single slot:



array("allowed_types" => array("aText", "myCustomType"),

62 | P a g e
"type_options" => array(

"aText" => array("multiline" => 1))));


Here the `multiline` option specifies that all

`aText` slots in the area should have the

`multiline` option set.

=== Global Slots and Virtual Pages ===

Most of the time, you want the content of a slot to be specific to a page.

After all, if the content was the same on every page, you wouldn't need

more than one page.

However, it is sometimes useful to have editable content that appears

on more than one page. For instance, an editable page footer or page

subtitle might be consistent throughout the site, or at least throughout

a portion of the site.

The quickest way to do this is by adding a "global slot" to your page template or layout.

Just set the `global` option to `true` when inserting the slot:


<?php a_slot('footer', 'aRichText',

array('toolbar' => 'basic', 'global' => true)) ?>


The content of the resulting slot is shared by all pages that include

63 | P a g e
it with the `global` option.

Note that you can use the `global` flag with areas as well as slots:


<?php a_area('footer', array(

'allowed_types' => array('aRichText', 'aImage'),

'global' => true

)) ?>


By default, global slots can be edited only by users with editing

privileges throughout the site. Otherwise users with control only over

a subpage could edit a footer displayed on all pages. See below for more information about

how to override this rule where appropriate.

==== Virtual Pages: When Global Slots Are Not Enough ====

Global slots will do the job for most situations that front end developers will encounter. If you're not
a PHP developer looking to use slots and areas to manage dynamic content related to your own
code, it's probably safe to skip this section, although it is sometimes useful to apply these techniques
if you would otherwise have hundreds of global slots in your design.

Conceptually, all global slots reside together on a virtual page with the slug `global`. Since there is no
leading `/`, this page can never be navigated to. The `global` virtual page resides outside of the site's
organizational tree and is used only as a storehouse of shared content.

For headers and footers, this works very well. But if you have a larger amount of shared content, the
model begins to break down. Fortunately there's a solution: group your content into separate virtual

64 | P a g e
When you include a slot this way:


<?php a_slot('biography', 'aRichText', array('toolbar' => 'basic', 'slug' => "bio-$id")) ?>


You are fetching that slot from a separate virtual page with the slug `bio-$id`, where `$id` might
identify a particular user in the `sfGuardUser` table.

Apostrophe will automatically create the needed virtual page object in the database the first time
the slot is used. This technique can be used for areas as well.

But why is this better than using `'global' => true`? Two reasons: performance and access control.

==== Performance, Global Slots and Virtual Pages ====

Yes, you could manage dynamic content like biographies with just the global flag, if you chose
dynamically generated names for your slots and areas. But since Apostrophe loads all current global
slots into memory the first time a global slot is requested on a page, the CMS would be forced to
load all of your biographies on just about every page of the site. That's clearly not acceptable. For
that reason it's important to use a separate virtual page for each individual's biography, etc.

As a rule of thumb, add database IDs to virtual page slugs, not area or slot names.

==== Access Control For Global Slots and Virtual Pages ====

By default, only sitewide admins can edit slots included from other virtual pages. This is fine for
headers and footers seen throughout the site, but doesn't work well for biography slots. In other
words, users should be able to write their own autobiographies.

You can address this problem by passing `'edit' => true` as an option to the slot or area. This
overrides the normal slot editing privilege checks, so you should do this only if the user ought to
have the privilege according to your own judgment. For instance, you might do something like this:

65 | P a g e

<?php $myid = sfContext::getInstance()->getUser()->getGuardUser()->id ?>

<?php a_slot('biography', 'aRichText', array('toolbar' => 'basic', 'slug' => "bio-$id", 'edit' => $id ===
$myid)) ?>


Once again, you can do this with areas as well. Also note that setting 'edit' => false will also disable
editing of the slot or areas for admin users as well, this is the behavior in the trunk and will be
present in 1.1

==== Including Slots From Other "Normal" Pages ====

It's possible to use the `slug` option to include a slot from another normal, navigable page on the
site. However, if you do so, keep in mind that you don't want to create a situation where the same
slot is included twice on the page itself.

Also keep in mind that normal pages can be moved around on the site, which will break templates
that contain explicit slugs pointing at their old locations. You can avoid this by determining the slug
option dynamically, or by using a virtual page instead (no leading / on the slug).

=== CSS: Styling Apostrophe ===

By default, `apostrophePlugin` provides two stylesheets which are

automatically added to your pages. There's a lot happening there,

particularly with regard to the editing interface, and we recommend

that you keep these and override them as needed in a separate

stylesheet of your own. However, you can turn them off if you wish

in `app.yml`:



66 | P a g e

use_bundled_stylesheet: false


If you do keep our stylesheets and further override them, you'll

want to specify `position: last` for the stylesheets that should

override them. This is automatically done for the stylesheet

`web/css/main.css` in our sandbox project, and we recommend you

make your customizations there.

==== The Golden Rule: Use Classes, Not IDs ====

You may want to style individual areas and slots without introducing

wrapper divs to your templates. To do that, pay attention to the CSS

classes we output on the outermost wrappers of each area. You'll note

that standalone slots still have an area wrapper in order to implement

their editing controls and make it easier to write consistent CSS:


<div id="a-area-12-body" class="a-area a-area-body">


You may be tempted to use the `id` attribute. ''Don't do that.'' The id attribute contains the page ID,
which differs from page to page and should never be used in CSS.

Instead, take advantage of the classes on this div:



67 | P a g e

css rules specific to the area or slot named 'body'


Note that if you need different behavior on different pages in a way that can't be achieved by adding
different slots and using different variants of each slot, you should use a separate template for each
type of page.

==== Why So Many Wrappers? ====

The wrapper divs output by Apostrophe are necessary to implement inline editing. We've found over
time that it's best to keep them in place even when the page is rendered in a non-editing mode in
order to allow a single set of CSS to render the page consistently.

We recommend embracing our wrapper divs and styling them to suit your needs rather than
attempting to remove or replace them or put unnecessary wrappers around them. See the provided
`a/templates/defaultTemplate.php` for an example of how to float one to the left of another. The
use of CSS floating makes it straightforward to lay slots and areas out as you see fit.

==== Styling Slot Variants ====

Slot variants (choices on the "Options" menu for a given slot) also set CSS classes. These classes have
the same name as the option. This is very useful for styling purposes, especially if you wish to display
the rich text description of a media image to the right or left of the image itself. See "Slot Variants:
More Mileage From Your Templates" for details.

=== CSS: Apostrophe UI ===

Apostrophe UI colors can be easily changed by overriding a small set of CSS styles bundled with the

Located at the bottom of a.css you will find this css:

68 | P a g e

/* 34. aUI & Admin Colors - Default





#a-global-toolbar #the-apostrophe

{ /* Apostrophe */

background-color: rgb(255,150,0);

background-color: rgba(255,150,0,0.75);





ul.a-controls .a-variant-options

{ /* Border Color */

border-color: rgb(255,150,0);

.a-admin .a-admin-content a:link,

.a-admin .a-admin-content a:visited,

#a-global-toolbar #a-logged-in-as span,

#a-personal-settings-heading span

{ /* Text Color */

69 | P a g e
color: rgb(255,150,0);


You can simply copy this section from a.css into your site's CSS file and change the values to colors
that work for your design.

==== aUI() ====

There is a javascript function `aUI();` that decorates the buttons in a cross-browser compatible way
on Dom Ready. It lives in `aUI.js`. By default it touches every button the page and applies functional
and aesthetic changes to the buttons. The `aUI()` call can be scoped using a CSS selector or a jQuery
object by simply passing it into the function call.

`aUI('.a-area-body');` will only affect buttons that are children of this CSS selector. We use this for
updating buttons that are returned via AJAX, missing out on the Dom Ready call.

==== aOverrides() ====

This function is hooked into `aUI()` To use it, define the function `aOverrides()` in a newly created a
project level `site.js` file. If it exists, `aUI()` will call it whenever `aUI()` is executed. This is helpful
when working with technologies such as `Cufon` that need to be run on Dom Ready.

=== CSS: Apostrophe Buttons === #abtn


* The base class for all buttons in the Apostrophe UI. When used alone, it will create
a simple button out of an any element.

* examples

* ''<A href="" class="a-btn">..text..</A>''

* ''<INPUT type="SUBMIT" value="..text.." class="a-btn"/>''

70 | P a g e
* ''<LI class="a-btn">...text...</LI>''

* '''.icon'''

* Alters the button to make space for a sprite on the left side

* '''.a-icon-name'''

* A second class is necessary for the .icon class to make sense. A

sprite is defined by the icon name class in the a.css file

* example


* ''.a-edit { background-image:
url(/apostrophePlugin/images/a-icon-edit.png); }''


* ''<A href="" class="a-btn icon a-edit">..text..</A>''

* '''.alt'''

* chooses the alternate CSS sprite and button color scheme for the button.

* some site designs work better with white icons instead of black and using
the .alt class in some situations helps improve the buttons legibility on a case-by-case basis. ''Note:
The .alt class can be applied to the <BODY> to globally change all buttons across the site to use the
alternate sprite.''

* '''.nobg'''

* removes background and border

* example

* ''<A href="" class="a-btn nobg">..text..</A>''

* '''.no-label'''

* Hides the text label of the button

* '''.mini'''

* Small button style with 10px type size

* '''.big'''

* Large button style with 18px type size

* example

71 | P a g e
* ''<A href="" class="a-btn big">..text..</A>''

* '''.flag'''

* Hides the text label and displays it upon :hover

* '''.flag-left'''

* Hides the text label and displays it as a tooltip to the left of the

* '''.flag-right'''

* Hides the text label and displays it as a tooltip to the right of the

* example

* ''<A href="" class="a-btn flag flag-right">..text..</A>''

=== Access Control: Who Can Edit What? ===

By default, an unconfigured Apostrophe site that was not copied from our sandbox follows these
security rules:

* Anyone can view any page without being authenticated.

* Any authenticated (logged-in) user can edit any page,

and add and delete pages.

This is often sufficient for simple sites. But Apostrophe can

also handle more complex security needs.

=== Requiring Login to Access All Pages ===

To require that the user log in before they view any page

in the CMS, use the following setting in `app.yml`:

72 | P a g e



view_login_required: true


=== Requiring Login to Access Some Pages ===

To require the user to log in before accessing a particular page,

just navigate to that page as a user with editing privileges

and click on the "lock" icon.

By default, locked pages are only accessible to logged-in users. Of course, on some sites this is too
permissive, especially when users are allowed to create their own accounts without further
approval. In such situations you can set up different credentials to access the pages. To require the
`view_locked` credential to view locked pages, use the following app.yml setting:




view_locked_sufficient_credentials: view_locked


Then grant the `view_locked` permission to the appropriate sfGuard groups, and you'll be able to
distinguish user-created accounts from invited guests.

=== Requiring Special Credentials to Edit Pages ===

Editing rights can be controlled in several ways. It may seem a bit confusing, so keep in mind that the
default set of permissions, groups and `app.yml` settings in our sandbox project works well and
allows the admin to assign editing and managing privileges anywhere in the CMS page tree as they
see fit. Editing privileges allow users to edit a page, while managing privileges allow them to also
create and delete pages.

73 | P a g e
Read on if you believe you may need to override this configuration. You will need to do that if you
are not using our sandbox project as a starting point, as the default behavior of the plugin is to allow
any logged-in user to edit as they see fit (often quite adequate for small sites without unprivileged
user accounts).

Editing and managing privileges are granted as follows:

1) Any user with the `cms_admin` credential can always

carry out any action in the CMS, regardless of all other settings. Note that the

sfGuard "superadmin" user always has all credentials.

2) Any user with `edit_sufficient_credentials` can always edit

pages (but not necessarily add or delete them) anywhere on the site. For instance,

if you add such users to the `executive_editors` sfGuardGroup and grant that

group the `edit` permission, then you can give them

full editing privileges with these settings:




edit_sufficient_credentials: edit


''If you do not specify any editing credentials at all, then any logged-in user can edit anywhere.'' This
is useful for small sites.

Similarly, any user with `manage_sufficient_credentials` can always

add or delete pages anywhere on the site, in addition to editing content. So

74 | P a g e
complete settings might be:




edit_sufficient_credentials: edit

manage_sufficient_credentials: manage


''Note that if you do not specify `manage_sufficient_credentials` any logged-in user can manage
pages anywhere.''

For convenience, you can also grant these privileges directly via a group name:




edit_sufficient_group: executive_editors

manage_sufficient_group: executive_editors


3) Any user who is a member of the group specified by

`app_a_edit_candidate_group` can ''potentially'' be made an

editor in particular parts of the site. If

`app_a_edit_group` is not set, all logged-in users are potential editors.



75 | P a g e

edit_candidate_group: editors


Similarly, any user who is a member of the group specified by

`app_a_manage_candidate_group` can potentially be given the ability to add

and delete pages in a particular part of the site. So a

common setup might be:




edit_candidate_group: editors

manage_candidate_group: editors


Why is this feature useful? Two reasons: because checking their membership

in one group is faster than checking their access privileges in the

entire chain of ancestor pages, and because when an administrator is

managing the list of users permitted to edit a page the list of users in the

editors group is much easier to read than a list of all users (especially

in a large system with many non-editing users).

4) Editing privileges for any specific page and its descendants

can be granted to any member of the group specified by

`app_a_edit_candidate_group` (if that option is set), or to

any user if `app_a_edit_candidate_group` is not set.

76 | P a g e
When a user with the right to manage a page opens the page settings,

they are given the option of assigning editors for that page.

The same principle applies to "managing" (adding and deleting)

pages, with the candidate group being indicated by

your `app_a_manage_candidate_group` setting.

Note that the pulldown list of possible editors can be quite long

if there are thousands of people with accounts on your site! This

is why we recommend setting up groups as described above.

=== Publishing Pages, by Choice and By Default ===

Apostrophe offers a "published/unpublished" toggle under "manage page settings."

Pages that are unpublished are completely invisible to users who do not

have at least the candidate credentials to be an editor; a user without appropriate privileges

gets a 404 not found error just as if the page did not exist. In most cases

you should use this in preference to actually deleting the page because the

content is still available if you choose to bring it back later.

By default all new a pages are in the "published" state.

If you need to approach the matter more conservatively, you can easily

change this with the following `app.yml` setting:




default_on: false


77 | P a g e
=== Limiting The Depth of the Page Tree ===

By default, editors can create pages nested as deeply as they wish. This makes sense for a site built
by a few skilled people, but when there are many cooks in the kitchen you may want to impose
some discipline. You can do that in `app.yml` by setting the `app_a_max_page_levels` option:




# Allows tabs, grandchildren, and great-grandchildren

max_page_levels: 3


A setting of `3` allows tabs, grandchildren (children of tabs), and great-grandchildren. This is often a
good choice. For a very simple site with no breadcrumb trail in the layout, you might want to use
`max_page_levels: 1`, which only permits tabs, or `max_page_levels: 2`, which only permits tabs and

=== Limiting the Number of Children of Any One Page ===

Similarly, you can limit the number of child pages that any given page can have. This helps to avoid
unwieldy side navigation and impose a bit more structure.

Set the `max_children_per_page` option to do this:




# No page, including the home page, may have more than 8 direct children

max_children_per_page: 8


78 | P a g e
Note that only the immediate children of a page are counted against this limit.

=== Deferring Search Engine Updates ===

By default pages are reindexed for search purposes at the time

edits are made. This makes installation simple, but for performance reasons

you might be happier deferring this to a cron job that runs every few minutes. If you want to take

approach, set up a cron job like this:


0,10,20,30,40,50 '' '' '' '' /path/to/your/project/symfony apostrophe:update-search-index --



Note the `--env` option. There is a separate index for each

environment. On a development workstation, specify `--env=env`. In

a production environment you might specify `--env=prod`.

Then turn on the feature in app.yml:




defer_search_updates: true


This speeds up editing a bit. But if you don't like cron, you don't have to enable it.

79 | P a g e
You can also change the word count of search summaries:




search_summary_wordcount: 50


=== Rebuilding the Search Index ===

When you deploy from development to production, you will need to build a new search engine index
for production. This is normally a one-time operation:


./symfony apostrophe:rebuild-search-index --env=prod


If you choose to sync your content to or from staging and production servers with
sfSyncContentPlugin, you'll need to run this task with the appropriate environment parameter for
the host in question after syncing content to it.

=== Conclusion ===

That's it for the front end designer oriented section of the manual. Next we'll move on to
information for PHP developers who want to extend Apostrophe with new capabilities and integrate
it more tightly into their own websites.

[ManualDevelopersGuide Continue to Developer's Guide]

80 | P a g e
[ManualOverview Up to Overview]

81 | P a g e
= Apostrophe Manual =

[ManualOverview Up to Overview]

== Developer's Guide ==

At this point you should already be familiar with the preceding material, which covers Apostrophe
installation, the end-user experience of managing an Apostrophe site, and front end design issues.
The designer's guide in particular is required reading to make good use of the following section,
which is devoted to extending Apostrophe in new ways with your own code.

We strongly recommend that you complete the [http://www.symfony-

project.org/jobeet/1_4/Doctrine/en/ Symfony tutorial] before attempting to add new features to
Apostrophe. We'll assume familiarity with Symfony development in this section.

=== Creating Custom Slot Types ===

You are not limited to the slot types provided with

apostrophePlugin! Anyone can create new slot types by taking

advantage of normal Symfony features: modules, components,

actions, templates and Doctrine model classes.

You can speed this process enormously by using the `apostrophe:generate-slot-type` task:


./symfony apostrophe:generate-slot-type --application=frontend --type=mynewtypename


This task generates all of the scaffolding for a new, working slot type. The above example generates
the necessary module, model class and form class in the `frontend` application of the project. You
can also generate the scaffolding in an existing or new Symfony plugin:

82 | P a g e

./symfony apostrophe:generate-slot-type --plugin=mynewplugin --type=mynewtypename


We recommend the latter as it is easier to reuse your slot type in another project this way. '''Don't
put your slot in apostrophePlugin itself'''. Make a plugin of your own, or add the slot at the project
level. This way you won't have problems later when you update apostrophePlugin.

'''Reminder:''' to activate your slot you must add it to the list of allowed slot types for your project in
`app.yml`, and also include it in the `allowed_types` option for individual Apostrophe areas in which
you want to allow editors to add it. In `app.yml`, you add a new slot type like this:





mynewtypename: "Nice Label For Add Slot Menu"


When inserting an area in a template or layout, you specify the allowed slot types like this:


a_area('body', array('allowed_types' => array('aRichText', 'mynewtypename')));


You can also use your new slot type in standalone slots with the `a_slot` helper.

Of course, to understand how to customize the behavior of your new slot type, you need to know
how slot types work. So let's dig into the workings of the `aFeed` slot, which was generated with the
`apostrophe:generate-slot-type` task and then edited to implement its own features.

83 | P a g e
1) A module. In this case, the `aFeedSlot` module. This contains actions, components and partials to
edit and render the slot within a page. '''If the module lives in a plugin, don't forget to enable the
plugin in your !ProjectConfiguration class and the module in your settings.yml file.'''

2) A form class, such as `aFeedForm`.

3) A model class, such as `aFeedSlot`, which inherits from the `aSlot` model class via Doctrine's
column aggregation inheritance feature. (Yes, we set this up for you when you use the
`apostrophe:generate-slot-type` task.)

==== Slot Modules, Part I: Edit and Normal Views ====

With a few notable exceptions like our aImage slot, most slots have an "edit view" and a "normal
view," rendered by editView and normalView components in the slot's module. The normal view
presents the slot as a user will see it when not editing. The edit view displays the same slot as the
user will see it after clicking the "Edit" button. For better editing performance, both views are
present in the HTML whenever the logged-in user has sufficient privileges to edit the slot.

A simple _editView partial just echoes the form class associated with the slot:

<?php echo $form ?>

Of course, you can render the form differently if you wish.

A slot type generated with the `apostrophe:generate-slot-type` task will already contain the
necessary supporting code in the `editView` component, in


public function executeEditView()

84 | P a g e
// Must be at the start of both view components


// Careful, don't clobber a form object provided to us with validation errors

// from an earlier pass

if (!isset($this->form))

$this->form = new aFeedForm($this->id, $this->slot->getArrayValue());


Notice that the form is initialized with two parameters: a unique identifier that distinguishes it from
other forms in the page, and a value fetched from the slot. Most slots store their data in the `value`
column of the slot table, using `serialize` and `unserialize` to store PHP data in any way they see fit.
The `aSlot::getArrayValue` and `aSlot::setArrayValue` methods are conveniences that simplify this
for you. `aSlot::getArrayValue` always returns a valid array, even if the slot is new (in which case the
array will be empty). And `aSlot::setArrayValue` accepts an array and serializes it into the `value`
column. You don't need to worry about any of that... although it is possible for you to implement
custom database columns instead of using Doctrine column aggregation inheritance as explained
below. This is usually not worth the trouble and the database schema changes it causes. However it
can be worthwhile if you need foreign key relationships and must avoid extra queries. You can also
approach that problem by referencing the id column of the `a_slot` table from a foreign key in a
related table, rather than the other way around.

The normal view takes advantage of the information stored in the slot to render it with its normal,
non-editing appearance. The feed slot's normal view component fetches the feed, via the web or via
an sfFileCache object if it has already been fetched recently, and the normal view partial renders it.

Your normal view's implementation is up to you. Try the slot type generator task to see a very simple
working example. One thing your normal view must do is provide an edit button at the top of the
partial, or provide some other way to edit the slot's settings. The task generates code like this in the
`_normalView` partial:


85 | P a g e
<?php include_partial('a/simpleEditWithVariants', array('name' => $name, 'permid' => $permid,
'pageid' => $pageid, 'slot' => $slot)) ?>


This code displays the edit button, and also offers a menu of slot variants if any have been
configured for this slot type on this particular site.

==== Slot Forms ====

Most slots, specifically slots that have an edit button and take advantage of the edit view
component, will have a form associated with them. By default this form is automatically echoed by
the edit view component, fully initialized and complete with any validation errors from unsuccessful

Here's the `aFeedForm` class:


class aFeedForm extends sfForm

// Ensures unique IDs throughout the page

protected $id;

public function __construct($id, $defaults)

$this->id = $id;



public function configure()

$this->setWidgets(array('url' => new sfWidgetFormInputText(array('label' => 'RSS Feed URL'))));

86 | P a g e
$this->setValidators(array('url' => new sfValidatorUrl(array('required' => true, 'max_length' =>

// Ensures unique IDs throughout the page

$this->widgetSchema->setNameFormat('slotform-' . $this->id . '[%s]');



Notice that this form class is '''not''' a Doctrine form class. Yes, `aFeedSlot` does inherit from `aSlot`
via Doctrine column aggregation inheritance. However, Doctrine forms do not distinguish between
column aggregation inheritance subclasses and will include all of the columns of all of the subclasses.
Also, it's usually best to avoid custom columns and use `setArrayValue` and `getArrayValue` instead,
which requires that we set up our own fields in the form class.

This class is only slightly changed from what the task generated. The `url` field has been added and
given a suitable label and a limit of 1024 characters. The name format has been set in the usual way
for a slot form and should not be changed (for a rare exception check out aRichTextForm, which
must cope with certain limitations of FCK). The form formatter in use here is the Apostrophe form
formatter, which produces nicely styled markup, but you may use another or render the form one
element at a time in the `_editView` partial.

==== Slot Components, Part II: Actions ====

The edit action of your slot's module saves new settings in the form, or reports a validation error and
refuses to do so.

Here's the edit action for aFeedSlot, which is exactly as the `apostrophe:generate-slot-type` task
generated it:


public function executeEdit(sfRequest $request)

87 | P a g e

$value = $this->getRequestParameter('slotform-' . $this->id);

$this->form = new aFeedForm($this->id, array());


if ($this->form->isValid())

// Serializes all of the values returned by the form into the 'value' column of the slot.

// This is only one of many ways to save data in a slot. You can use custom columns,

// including foreign key relationships (see schema.yml), or save a single text value

// directly in 'value'. serialize() and unserialize() are very useful here and much

// faster than extra columns


return $this->editSave();


// Makes $this->form available to the next iteration of the

// edit view so that validation errors can be seen, if any

return $this->editRetry();


This action begins by calling `$this->editSetup()`, which takes care of determining what slot the
action will be working with. The action then fetches the appropriate parameter, binds the form,
validates it, and if successful saves the form's data in the slot with `setArrayValue` and calls `$this-
>editSave()` to save and redisplay the slot. If there is a validation error, `$this->editRetry()` should be
called instead.

88 | P a g e
Depending on your needs you might not need to modify this action at all. The methods called by this
action take care of version control, access control and everything else associated with the slot and
let you get on with your job.

==== Custom Validation ====

Sometimes `$this->form` isn't quite enough to meet your needs. You might

have more than one Symfony form in the slot (although you should

look at `embedForm()` and `mergeForm()` first before you say that).

Or you might not be using Symfony form classes at all.

Fortunately there's a way to pass validation messages from the

`executeEdit` action to the next iteration of the `editView` component:


// Set it in the action

$this->validationData['custom'] = 'My error message';

// Grab it in the component

$this->error = $this->getValidationData('custom');

// ... And display it in the template

<?php if ($error): ?>

<h2><?php echo $this->error ?></h2>

<?php endif ?>


Note that `$this->validationData['form']` is used internally

to store `$this->form`, if it exists in the action. So we suggest

that you use other names for your validation data fields.

89 | P a g e
==== Additional Actions ====

Things get interesting when you need to edit your slot with additional actions, possibly actions that
go to different pages like the "Choose Image" buttons of our media slots.

You can write your own actions that use `$this->editSetup()`, `$this->editSave()` and `$this-
>editRetry()`. The tricky part is linking to these actions, from your normalView partial or elsewhere.
Here's an example of a possible target for a form submission or redirect that would successfully
invoke such an action:


url_for('mySlot/myAction') . "?" .



"slot" => $name,

// The slug of the page where the slot lives.

// Could be a virtual page if this is a global slot etc.

"slug" => $page->slug,

// The actual page we were looking at when we began editing the slot

"actual_slug" => aTools::getRealPage()->getSlug(),

"permid" => $permid,

// Optional: use this if you are redirecting

// from another page and need the entire page to render

"noajax" => 1))


For a fully worked example, we recommend taking a look at the `aButtonSlot` module, which uses
both an edit view with a form (for the title and link) and a separate action that eventually redirects
back (for selecting the image).

==== Adding Database Columns ====

90 | P a g e
The `apostrophe:generate-slot-type` task takes care of setting up the model classes for you so that
Apostrophe can distinguish between your slot and other slots, even though all of them are stored in
the `a_slot` table.

If you are using `getArrayValue` and `setArrayValue` or otherwise storing your data in the `value`
column, you'll never have to worry about this directly.

However, if you do need to add custom columns, you'll need to know how this looks in

Here's how `aFeedSlot` is configured:



# Doctrine doesn't produce useful forms with column aggregation inheritance anyway,

# and slots often use serialization into the value column... the Doctrine forms are not

# of much use here and they clutter the project



form: false

filter: false

# columns:

# You can add columns here. However, if you do not need foreign key relationships it is

# often easier to store your data in the 'value' column via serialize(). If you do add columns,

# their names must be unique across all slots in your project, so use a unique prefix

# for your company.

# This is how we are able to retrieve slots of various types with a single query from

91 | P a g e
# a single table


extends: aSlot

type: column_aggregation

keyField: type

keyValue: 'aFeed'


Take a look at the `inheritance` section. The `extends` keyword specifies the class we are inheriting
from, while the `keyValue` field must contain the name of the type. Doctrine uses

this to figure out what class of object to create when loading a

record from the `a_slot` table. The slot type name is

recorded in the `type` column, already in the `aSlot` class. You don't need to worry

about the details, but for more information about them,

see the excellent Doctrine documentation.

''Note that the keyValue setting does not include the word Slot.''

To add extra columns at the database level, uncomment `columns:` and add new columns precisely
as you would for any Doctrine model class. You can add new relations as well.

with other slots. Doctrine does not do this automatically, so please take care to avoid names that
may lead to conflicts down the road.

==== Opening the Edit View Automatically For New Slots ====

By default, when a user adds a new slot to an area the user must then click the

edit button before making changes to the slot. To take the user straight to

the editView of your slot, you have to set the member variable

92 | P a g e
`editDefault` to `true`. See the aRichTextSlot or aTextSlot for an example.

If your slot lives in a plugin, the right class to edit is

`plugins/myPlugin/lib/model/doctrine/PluginmySlot.class.php`. If your slot lives at the project level,
the right file is `lib/model/doctrine/mySlot.class.php`.

=== Managing Global Admin Buttons to the Apostrophe Admin Menu ===

When a user with editing privileges is logged in and visiting a page for which they have such
privileges, a bar appears at the top of each page offering links to appropriate administrative
features. Admins will see a button offering access to the sfGuardUser admin module. Editors in
general will have access to the media module. You can add links of your own, or change the order of
the buttons to be displayed.

==== Adding, Removing and Reordering Buttons via `app.yml` ====

The simplest way to add new buttons is to set `app_a_extra_admin_buttons` in `app.yml`. This
allows you to add buttons that point to Symfony actions you coded yourself. By default, this is
equivalent to:






label: Users

action: 'aUserAdmin/index'

class: 'a-users'


label: Reorganize

action: a/reorganize

class: a-reorganize

93 | P a g e

If you override this setting you will almost certainly want to keep these two buttons in the list.

Note that the key and the `label` field are different. `label` is shown to the user, and automatically
internationalized by Symfony. The label is also looked for in the `apostrophe` i18n catalog. The key is
used in `app.yml` settings to reorder buttons.

Note that the built-in media repository will always add a Media button to this list (with the name
`media`), and the blog plugin, if installed, will add `Blog` and `Events` buttons.

To specify the order of the buttons, or discard buttons that were added by plugins, use





- users

- reorganize

- media

- blog


Global buttons will be displayed in the order specified here (specify button keys, not button labels).
Any buttons you leave off the list will not be displayed at all, even if they were added by plugins, the
media repository, etc.

If you do not specify `app_a_global_button_order` the buttons will be displayed in alphabetical

order by name.

94 | P a g e
==== Adding Buttons Programmatically ====

`app.yml` is the easiest way to add buttons, but sometimes it's not enough. Perhaps you're writing a
plugin that adds new features to Apostrophe and should register new buttons on its own. Or
perhaps you want to add a button that targets a specific engine page. You can do both of these
things by responding to the `a.getGlobalButtons` event.

First provide a static method in a class belonging to your own plugin or application-level code which
invokes `aTools::addGlobalButtons` to add one or more buttons to the bar:


class aMediaCMSSlotsTools

// You too can do this in a plugin dependent on apostrophePlugin, see

// the provided stylesheet for how to correctly specify an icon to go

// with your button. See the apostrophePluginConfiguration class for the

// registration of the event listener.

static public function getGlobalButtons()


new aGlobalButton('media', 'Media', 'aMedia/index', 'a-media')));


The first argument to the `aGlobalButton` constructor is the name of the button. This is used to refer
to that button elsewhere in your code and in `app.yml`. The second argument is the label of the
button, which may contain markup and will be automatically internationalized. The third is the
action (in your own code, typically). And the fourth is a CMS class to be added to the button,

95 | P a g e
which is typically used to supply your own icon and a left offset for the image to reside in.

If your own plugin, like our media system, implements its administrative page as an apostrophe CMS
engine page under `/admin` and also might have public engine pages elsewhere on the site, you'll
want to make sure your button targets the "official" version. You can do that by providing the right
engine page as a fifth argument to the aGlobalButton constructor:


static public function getGlobalButtons()

$mediaEnginePage = aPageTable::retrieveBySlug('/admin/media');

// Only if we have suitable credentials

$user = sfContext::getInstance()->getUser();

if ($user->hasCredential('media_admin') || $user->hasCredential('media_upload'))


new aGlobalButton('media', 'Media', 'aMedia/index', 'a-media', $mediaEnginePage)));


For more information see "Engines: Grafting Symfony Modules Into the CMS Page Tree" below.

Now, in the initialize method of your plugin or project's configuration class, make the following call
to register interest in the event:


// Register an event so we can add our buttons to the set of global

// CMS back end admin buttons that appear when the apostrophe is clicked.

96 | P a g e

array('aMediaCMSSlotsTools', 'getGlobalButtons'));


The bar at the top of each page will now feature your additional button or buttons.

''Note:'' you should not add large numbers of buttons to the bar. Usually no more than one per
plugin is advisable. It's important that the bar remain manageable and convenient for site admins.

=== Engines: Grafting Symfony Modules Into the CMS Page Tree ===

Suitably coded Symfony modules can now be grafted into the page tree at any point in a flexible way
that allows admins to switch any page from operating as a normal template page to operating as an
engine page, with all URLs beginning with that page slug remapped to the actions of the engine
module. When the engine page is moved within the site, all of the virtual "pages" associated with
the actions of the module move as well.

''A single engine module can now be grafted into more than one location on a site.'' To take
advantage of this feature, you must disable the Symfony routing cache. Disabling the routing cache
is the default in Symfony 1.3 and 1.4 because the routing cache causes performance problems rather
than performance gains in most cases (and in some cases they are quite severe and unpredictable).
However, if you require the Symfony routing cache, you can still use engines as long as you don't
install the same engine at two points in the same site. Even without multiple instances, engines still
allow components such as a staff directory to be located at the point in the site where the client
wishes to put them without the need to edit configuration files.

Engine modules are written using normal actions and templates and otherwise-normal routes of the
aRoute and aDoctrineRoute classes.

This is a very powerful way to integrate non-CMS pages into your site. The media browser of
apostrophePlugin already takes advantage of it, and the forthcoming apostropheBlogPlugin will as

97 | P a g e
Engines should always be used when you find yourself wishing to create a tree of dynamic "pages"
representing something other than normal CMS pages, beginning at a point somewhere within the
CMS page tree.

To create a a engine, begin by creating an ordinary Symfony module. Feel free to test its
functionality normally at this point. Then change the parent class from `sfActions` to

NOTE: if your actions class has a `preExecute` method of its own, be sure to call `parent::preExecute`
from that method. Otherwise it will not work as an engine.

''If your actions class must have a different parent class'', implement your own `preExecute()`
method in which you call Apostrophe's helper method for engine implementation. For example, this
admin generator actions class has been modified to work as an Apostrophe engine:


class departmentActions extends autoDepartmentActions

public function preExecute()




Now, create routes for all of the actions of your module, or a catch-all route for all of them. Make
sure you give these routes the `aRoute` class in `routing.yml`. The following are sample routes for a
module called `enginetest`:


98 | P a g e
# Engine rules must precede any catch-all rules


url: /

param: { module: enginetest, action: index }

class: aRoute


url: /foo

param: { module: enginetest, action: foo }

class: aRoute


url: /bar

param: { module: enginetest, action: bar }

class: aRoute


url: /baz

param: { module: enginetest, action: baz }

class: aRoute


You can also use more complex rules to avoid writing a separate rule for each action, exactly as you
would for a normal Symfony module. This example could replace the `foo`, `bar`, and `baz` rules



url: /:action

param: { module: enginetest }

class: aRoute

99 | P a g e

You can also use Doctrine routes. Configure them as you normally would, but set the class name to



url: /:slug

param: { module: aEvent, action: show }

options: { model: Event, type: object }

class: aDoctrineRoute

requirements: { slug: '[\w-]+' }


Finally, in the forthcoming version 1.5 (and in the current trunk), you can use an



class: aDoctrineRouteCollection


model: Department

module: department

prefix_path: ''

column: id

with_wildcard_routes: true


100 | P a g e
The above is the same route collection that `doctrine:generate-admin-module` added to
`routing.yml` automatically for this module, except that the class has been changed to
`aDoctrineRouteCollection` and the prefix path set to an empty string as a reminder that prefix paths
are not relevant for engines (the prefix path is automatically overridden to an empty string in any

In general, you may use all of the usual features available to Symfony routes.

Note that the URLs for these rules are very short and appear to be at the root of the site. `aRoute`
will automatically remap these routes based on the portion of the URL that follows the slug of the
"engine page" in question.

That is, if an engine page is located here:




And the user requests the following URL:


The `aRoute` class will automatically locate the engine page in the stem of the URL, remove the slug
from the beginning of the URL, and match the remaining part:




To the appropriate rule.

101 | P a g e
As a special case, when the engine page is accessed with no additional components in the URL,
`aRoute` will match it to the rule with the URL `/`.

Note that as a natural consequence of this design, engine pages cannot have subpages in the CMS.
In general, it is appropriate to use engines only when you wish to implement "virtual pages" below
the level of the CMS page. If you simply wish to customize the behavior of just part of a page, a
custom page template or custom slot will better suit your needs.

Once you have established your routes, you can create subnavigation between the actions of your
module by writing normal `link_to` and `url_for` calls:


echo link_to('Bar', 'enginetest/bar')


To make the user interface aware of your engine, add the following to `app.yml`:





'': 'Template-Based'

enginetest: 'Engine Test'


Substitute the name of your module for `enginetest`. Be sure to keep the "template-based" entry in
place, as otherwise normal CMS pages are not permitted on your site.

102 | P a g e
Linking to the "index" action of an engine page is as simple as linking to any other page on the site.
But what if you need to generate a link to a specific engine action from an unrelated page? For
instance, what if you wish to link to a particular employee's profile within an engine page that
contains a directory of staffers?

Just call `link_to` exactly as you did before:


echo link_to('Bar', 'enginetest/bar')


If the current page is not an engine page matching the route in question, the a routing system will
find the first engine page in the site that does match the route, and generate a link to that engine

Note: if there is currently no engine page for the given engine, this will throw an exception and
generate a 500 error. This makes sense: trying to generate a link to an engine page that doesn't exist
is a lot like trying to use a route that doesn't exist. You can test to make sure the engine page exists
like this:


<?php if (aPageTable::getFirstEnginePage('enginetest')): ?>

<?php echo link_to('Bar', 'enginetest/bar') ?>

<?php endif ?>


==== Which Engine Page Does My Link Point To? ====

When there is just one engine page on the site for a particular engine module, things are simple:
links to routes for that engine always point to a URL beginning with that page. With multiple
instances of the same module, things get trickier. Here's how to sort it out.

103 | P a g e
There are three simple rules:

1. When there is only one engine page on the site for a particular engine module 'blog', links always
target that page by default. For many purposes, this is all you need.

2. When the page for which the link is being generated (the current CMS page) is an engine page for
'blog', links generated on that page will point back to that page by default, even if other engine
pages for that engine module do exist. If the current page is not an engine page for the engine in
question, the first matching engine page found in the database is used by default.

3. When you wish to target a specific engine page with link_to and url_for calls, you add an extra
`engine-slug` parameter to the Symfony URL (beginning in Apostrophe 1.4):


<?php echo link_to('Jane's Blog', 'blog/index?engine-slug=/janesblog') ?>


The `engine-slug` parameter will automatically be removed from the URL and will not appear in the
query string. It is used only to determine which engine page to target. If you do not specify this
parameter, you get the first matching engine page, as explained above. If you have an `aPage` object
and wish to target it just set `engine-slug` to `$myPage->slug`.

There is an alternative to `engine-slug` which may be appropriate if you wish to override the engine
slug for a large block of code or a partial you are about to include:


<?php aRouteTools::pushTargetEnginePage('/janes-blog') ?>

<?php echo link_to('Jane's Blog', 'blog/index') ?>

<?php aRouteTools::popTargetEnginePage('blog') ?>


104 | P a g e
For convenience, you may pass either a page slug (like `/janes-blog`) or an `aPage` object to the
`aRouteTools::pushTargetEnginePage` method.

Now all aRoute and aDoctrineRoute-based URLs generated between the `push` and `pop` calls that
use the `blog` module will target the "Jane's Blog" page. URLs generated after the `pop` call revert to
the usual behavior.

Since you may find yourself writing partials and components that are included in other pages, it is
advisable to always `pop` after `push`ing a different engine page in order to avoid side effects.

We recommend using `engine-slug` rather than pushing and popping engine pages wherever
practical, as it adheres more closely to the philosophy of newer versions of Symfony which
emphasize dependency injection and frown on global state.

Again, you must not enable the Symfony routing cache if you wish to include multiple engine pages
for the same engine in your site. The routing cache is turned off by default in both Symfony 1.3 and
1.4. If you have upgraded an older project you may need to manually shut it off in

==== Extending the Page Settings Form: Creating an Engine Settings Form ====

"If I have separate engine pages for Jane's blog and Bob's blog, both using the blog engine, how do I
distinguish them?" That part is easy. Just use the `id` of the engine page as a foreign key in your own
Doctrine table and keep the details that distinguish them in that table. This is how our media
repository associates particular engine pages with particular media categories.

"But how can editors change settings for that particular engine page?" Well, you could create your
own settings action of course, and link to it from your engine page. Remember, Apostrophe is still
Symfony, and you can always do normal Symfony development.

But we also provide a convenient way to extend the page settings form that rolls down when you
click "This Page" and then click on the gear.

105 | P a g e
Assuming your engine module is called `blog`, this is all you have to do:

1. Create a form class called `blogEngineForm`. The constructor of this class must accept an `aPage`
object as its only argument.

2. Create a `blog/settings` partial that renders `$form`. This partial can be as simple as `<?php echo
$form?>` if you wish.

Apostrophe will automatically look for this form class. If it exists, Apostrophe will render both the
standard page settings form ```and``` your engine settings form if that engine is selected. Apostrophe
will automatically fetch your form on the fly if the user switches the template of the page to `blog`.

The page settings will ```not``` be saved unless ```both``` forms validate successfully. If both forms
validate, Apostrophe will save them consecutively (the page settings form, followed by your engine
settings form).

One simple way to create a form that works with this approach is to add a relation between `aPage`
and your own table in your application or plugin schema. Then you can extend the `aPageForm` class
and immediately remove all fields, then add back the fields you're interested in. Take a look at


class aMediaEngineForm extends aPageForm

public function configure()


$this->setWidget('media_categories_list', new sfWidgetFormDoctrineChoice(array('multiple' =>

true, 'model' => 'aMediaCategory')));

$this->widgetSchema->setLabel('media_categories_list', 'Media Categories');

$this->widgetSchema->setHelp('media_categories_list','(Defaults to All Cateogories)');

$this->setValidator('media_categories_list', new sfValidatorDoctrineChoice(array('multiple' =>

true, 'model' => 'aMediaCategory', 'required' => false)));

106 | P a g e




The corresponding schema is:



tableName: a_media_category


Timestampable: ~

Sluggable: ~



type: integer(4)

primary: true

autoincrement: true


type: string(255)

unique: true


type: string


107 | P a g e

class: aMediaItem

local: media_category_id

foreign: media_item_id

foreignAlias: MediaCategories

refClass: aMediaItemCategory

# Used to implement media engine pages dedicated to displaying one or more

# specific categories


class: aPage

local: media_category_id

foreign: page_id

foreignAlias: MediaCategories

refClass: aMediaPageCategory


This form takes advantage of the fact that Doctrine will automatically save the `MediaCategories`
relation when `save()` is called on the form, looking for a widget named `media_categories_list`.

You don't have to extend `aPageForm` and use a relation in this way, but if you don't you'll need to
make sure your `updateObject()` and `save()` methods do the right thing in your own way.

==== Testing Your Engine ====

After executing `symfony cc`, you will begin to see your new engine module as a choice in the new
"Page Engine" dropdown menu in the page settings form. Select your engine and save your changes.
The page will refresh and display your engine.

Note that engine pages can be moved about the site using the normal drag and drop interface.

108 | P a g e
You can create your own subnavigation within your engine page. We suggest overriding appropriate
portions of your page layout via Symfony slots.

=== Internationalization ===

Internationalization is supported at a basic level: separate versions

of content are served depending on the result of calling getCulture()

for the current user. When you edit, you are editing the version of

the content for your current culture. The user's culture defaults, as

usual, to the sf_default_culture settings.yml setting. The search index also

distinguishes between cultures. Webmasters who make use of internationalization will want

to add a "culture switcher" to their sites so that a user interface is

available to make these features visible. Thanks to Quentin

Dugauthier for his assistance in debugging these features.

The user interface for editors is not yet internationalized. We plan to do this in the future.

=== Refreshing Slots ===

This is not necessary for any of our standard slot types. However, if your custom slot types contain
metadata that should be refreshed nightly, you might wish to take advantage of the
`apostrophe:refresh` task, which updates all current slots by calling their `refreshSlot()` method:


./symfony apostrophe:refresh --env=prod --application=frontend


Again, currently our own media slots do not require the use of this task. Thanks to architectural
improvements deleting an item from the media plugin immediately updates the related slots. In
future we may implement a handshake with YouTube via this task to check whether video slots are
still pointing to valid videos.

109 | P a g e
For performance reasons this task only looks at the latest version of each slot. You can use the `--
allversions` option to specify that older versions of slots should be refreshed as well:


./symfony apostrophe:refresh --env=prod --application=frontend --allversions


=== Extending Search ===

You can override the a/search action at the application level. Just

like any other Symfony application.

You can do that in three ways. The easiest way is to override the

template that displays the results (a/searchSuccess), and bring in more results for

other types of data via a Symfony component of your own. You can grab

the search query string from $sf_data->getRaw('q').

This is Zend Lucene search, so Lucene syntax is allowed:


You can use Lucene to index your own data and take advantage of that

fully. Keep in mind that the entire Zend library is available to you

already since it's one of Apostrophe's requirements.

Following this approach you don't need to know much about Apostrophe's

internals at all.

110 | P a g e
The second supported approach is to store (or mirror) your additional data using Apostrophe virtual
pages, as introduced in ManualDesignersGuide. Virtual pages are included in Apostrophe search
results if the virtual page slug appears to be a valid Symfony URL: that is, if it begins with a `@` or
contains a `/` internally (not at the beginning).

apostropheBlogPlugin uses this technique to create valid links when a search matches a published
blog post. In the blog plugin, all content is stored as Apostrophe virtual pages, and the page slugs
look like this:




This is a powerful and effective approach but may not suit your needs if you don't wish to store or
mirror your custom content in Symfony slots.

The third supported way is to interleave your own results with the

page search results, based on their search ranking. This makes sense

only if the results are somewhat reasonable to compare - for instance, articles and web pages are
reasonably similar and Zend will probably

produce search rankings that mix reasonably well.

Fortunately, Apostrophe's a/search action is designed to be extended

in this way. All you have to do is implement the searchAddResults

method in your application-level aActions class, which should extend


The code below demonstrates how you might handle search results for a blog that chooses not to
use our virtual pages approach:


111 | P a g e
// This is at the application level,


class aActions extends BaseaActions

protected function searchAddResults(&$values, $q)

// $values is the set of results so far, passed by reference so

you can append more.

// $q is the Zend query the user typed.


// Override me! Add more items to the $values array here (note

that it was passed by reference).

// Example: $values[] = array('title' => 'Hi there', 'summary' =>

'I like my blog',

// 'link' => 'http://thissite/wherever', 'class' => 'blog_post',

'score' => 0.8)


// 'class' is used to set a CSS class (see searchSuccess.php) to

distinguish result types.


// Best when used with results from a

aZendSearch::searchLuceneWithValues call.


// IF YOU CHANGE THE ARRAY you must return true, otherwise it will

not be sorted by score.

// return true;

112 | P a g e


This method's job is to add more results to the results array (note

that it is passed by reference), which then get sorted by score with

everything else and presented as part of the search results.

If you are wondering how to integrate Zend Search into your own

modules, check out our aZendSearch class

(apostrophePlugin/lib/toolkit/aZendSearch.class.php), which does it

for our own data types. That class provides methods you can call from

your model classes to add search indexing to them, and also expects

your class to provide some methods of its own that get called back.

For a working example, see the aMediaItem class. The save() method calls:


// Let the culture be the user's culture

return aZendSearch::saveInDoctrineAndLucene($this, null, $conn);


That method calls back to your doctrineSave method, which is usually a

simple wrapper around parent::save for a Doctrine model class, but you

can extend it as needed:


113 | P a g e
public function doctrineSave($conn)

$result = parent::save($conn);

return $result;


And it also calls back to updateLuceneIndex, which should invoke

aZendSearch::updateLuceneIndex with an associative array of fields to

be included in the search index:


public function updateLuceneIndex()

aZendSearch::updateLuceneIndex($this, array(

'type' => $this->getType(),

'title' => $this->getTitle(),

'description' => $this->getDescription(),

'credit' => $this->getCredit(),

'tags' => implode(", ", $this->getTags())



The array we pass as the second argument to updateLuceneIndex contains

fields that should be indexed so that we can search on them, but not

stored in full for display purposes. That's great here because with

114 | P a g e
media we know we'll want to retrieve those objects from Doctrine later

anyway. But it is also possible to ask Lucene to actually store some

fields for you. And that is crucial if you want to display complete

search results with our unmodified a/search template.

Specifically, you'll need to store:

* The title ('title')

* The summary text ('summary')

* The URL ('url'), which can be a Symfony URL or a regular URL

* 'view_is_secure' (a boolean flag indicating whether logged-out users

and logged-in users without guest permissions are allowed to see this

search result)

So the complete call to aZendSearch::updateLuceneIndex might be:



array('text' => $this->getFullSearchText()),

null, // Or perhaps $this->getCulture() depending on your needs

array('title' => $this->title,

'summary' => $this->getShortSummary(),

'url' => 'mymodule/show?id=' . $this->id,

'view_is_secure' => false));


Note the second argument, which is the culture for this object. You can

115 | P a g e
pass null to use the current user's culture which often makes sense if

they have just edited the object. Apostrophe search returns only results for your current culture.

Your 'getFullSearchText' method would typically just append all of the

fields that contain text relevant to searching (title, tags, actual

body text) into a single string and return that.

You must also call aZendSearch::deleteFromDoctrineAndLucene from your

delete method (I'm leaving out some stuff specific to the media item

class here):


public function delete(Doctrine_Connection $conn = null)

return aZendSearch::deleteFromDoctrineAndLucene($this, null, $conn);


That method will call back to your doctrineDelete method, which is

usually just a wrapper around parent::delete:


public function doctrineDelete($conn)

return parent::delete($conn);


116 | P a g e
You may wonder why there are so many aZendSearch-related methods.

Basically, we would have used multiple inheritance here, but PHP

doesn't have it. So instead we provide helper methods in the

aZendSearch class which give us a flexible way to "inherit" the

searchable behavior without explicit support for multiple inheritance

in PHP.

=== Manipulating Slots Programmatically ===

==== Fetching Pages With Their Slots ====

Doctrine developers may be tempted to just use `findOneBySlug` and then iterate over areas and so
on. '''Don't do this.''' You will get all of the related objects for all versions of the page throughout
time, and the first one you get will not be the current version, nor will the slots in an area be in the
right order.

The correct way to fetch the home page is:


$page = aPageTable::retrieveBySlugWithSlots('/');


You can pass any page slug; the home page is just an example.

This method fetches the page with its ```current``` slots in the correct order.

117 | P a g e
Note that if you are writing a page template or partial you can get the current page much more
cheaply. In a page template you can just use the `$page` variable which is already correctly
populated for you. In a partial you can call `$page = aTools::getCurrentPage()`.

You can then fetch the slots of any area by name, in the proper order:


$slots = $page->getArea('body');


This suggests an easy way to check whether an area is empty:


if (count($page->getArea()) == 0)

// This area is currently empty


You can also fetch an individual slot by its area name and permid. Note that the permid of a
singleton slot (inserted with `a_slot` rather than `a_area`) is always 1:


$slot = $page->getSlot('footer', 1);


Usually you won't manipulate slot objects directly, but you may find the `$slot->getText()` method
useful in some situations. This method returns entity-escaped text for the slot. Normally you'll rely
on Apostrophe to display and edit slots via the `a_slot()` and `a_area()` helpers.

118 | P a g e
==== Advanced Queries for Pages ====

If you need to fetch more than one page, or have other Doctrine criteria for fetching the page, or
need to add additional joins, consider:


$query = aPageTable::queryWithSlots();

$query->whereIn('p.id', array(some page ids...));

$pages = $query->execute();


`aPageTable::queryWithSlots` returns a Doctrine query with the necessary joins to correctly populate
returned pages with the current versions of the correct slots.

==== Adding and Updating Slots ====

Usually you'll want to create a new slot type and let `BaseaSlotActions` and `BaseaSlotComponents`
do the dirty work for you. But sometimes you may want to manipulate slots directly.

You can modify a slot object and save it, but if you do, you're not

creating a history that the user can roll back.

To do that, make a *new* slot object and use newAreaVersion to add it

to the history:


$page = aPageTable::retrieveBySlugWithSlots('/foo');

$slot = $page->createSlot('aText');

119 | P a g e
$slot->value = $title;


$page->newAreaVersion('title', 'update',


'permid' => 1,

'slot' => $slot));


Note that you want to use a new slot object. Often it's easiest to

copy the previous version:


$slot = $slot->copy();

// Make your changes to $slot, then call newAreaVersion


If you want to add an entirely new slot, specify 'add' rather than

'update'. You do not have to specify a permid since that is generated

for you when adding a new slot:


$page->newAreaVersion('myareaname', 'add', array('slot' => $slot));


You can explicitly specify that it should or should not be at the top

of the area:

120 | P a g e

$page->newAreaVersion('myareaname', 'add', array('slot' => $slot,

'top' => false));


The default is to add the slot at the top.

==== Looping Over All Slots in a Page ====

Looping over slots is dangerous, depending on your goals, because

there can be slots that are not actually used in the current template

if a page has changed templates.

You can do it with a loop like this after you retrieveBySlugWithSlots:


foreach ($this->Areas as $area)

$areaVersion = $area->AreaVersions[0];

foreach ($areaVersion->AreaVersionSlots as $areaVersionSlot)

$slot = $areaVersionSlot->Slot;

$permid = $areaVersionSlot->permid;

// Now you can do things with $slot

121 | P a g e

Recall that areas can contain multiple slots (as a result of the "add

slot" button). The permid is the slot's unique identifier within its

area. All versions of the same slot will have the same permid. You

need the permid to save a new revision of the slot.

=== Adding Support For New Embedded Media Services ===

Out of the box, version 1.5 of Apostrophe supports embedding almost any media service (such as
Youtube) by pasting embed tags via the "Embed Media" button. In addition, Apostrophe has native
support for Youtube and Vimeo. That means that you can do the following things with Youtube and
Vimeo that you can't do with other services:

* Search for videos to add to the media repository via the "Search Services" button

* Use the "Linked Accounts" feature to automatically bring media into the repository

* Paste a video URL rather than a full embed code on the "Embed Media" page

* Avoid typing in the title, description and tags manually

* Automatically retrieve a thumbnail when adding the item

Beginning in Apostrophe 1.5 (and currently available in the svn trunk), you can add additional
services at the project level or in a Symfony plugin. All you have to do is extend the aEmbedService
class and implement the methods you find there, fetching the appropriate information from the API
for the service you're interested in.

The APIs of these methods have deliberately been kept very simple. You should have very little
trouble implementing them if you are comfortable with the service API you're talking to (and most
are very easy to work with). Documentation of the expected return values is provided in comments
in the aEmbedService class.

122 | P a g e
Although you can start from scratch, we recommend copying the aYoutube or aVimeo class as a
starting point, as it makes it easier to ensure you are returning data in the right format.

After you write your class, you'll need to tell Apostrophe about it with appropriate settings in





- class: aYoutube

media_type: video

- class: aVimeo

media_type: video

- class: aMyservice

media_type: video


Here the standard YouTube and Vimeo services have been kept in place. You can choose to remove
them if you wish by not including them in your settings.

(In Apostrophe 1.4 there was no support for adding new media services that receive the same
special treatment as Youtube, however pasting embed tags for most services is supported via the
"Add via Embed Code" option in the media repository. We recommend moving to the trunk and
soon to version 1.5 if you are interested in adding support for custom services.)

=== Conclusion ===

Thanks for checking out Apostrophe! We hope you're excited to experiment with our CMS. If you've
read this far, there's a good chance you'll build custom slots and engines... and even send us bug
reports. Hey, we love bug reports. Visit the [http://trac.apostrophenow.org/ Apostrophe Trac] to

123 | P a g e
submit your reports, requests and concerns. And also be sure to join the
[http://groups.google.com/group/apostrophenow apostrophenow Google group] to share your
experiences with other Apostrophe developers.

[wiki:ManualI18N Continue to Internationalizing Apostrophe]

[ManualOverview Up to Overview]

124 | P a g e
= Apostrophe Manual =

[ManualOverview Up to the Overview]

== Requirements ==

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

The '''quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the the

sandbox project]''', which includes the blog plugin, in which case you can just install that according
to [wiki:ManualInstallation#CheckItOutFromSubversion the directions].

== Installation ==

In a new project, creating the database tables for the blog plugin is a natural part of the usual


./symfony doctrine:build --all --and-load


Again, '''the quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the

1.4 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.

125 | P a g e
'''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


./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 ===

126 | P a g e
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:

1. Navigate to the home page

2. Log in as admin

3. Click "This Page," then "Add New Page," giving it the name "News" or "Blog" depending on your

4. When the new page appears, click "This Page," then click the "Gear" icon

5. Set "Page Engine" to "Blog"

6. 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."

127 | P a g e
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.

128 | P a g e
* `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.

== Customizing Blog Post Templates ==

All blog posts 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):






name: Single Column

areas: ['blog-body']


name: Two Column

areas: ['blog-body', 'blog-sidebar']

comments: false

# add_this: punkave # Username for AddThis -- http://addthis.com


129 | P a g e


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`.

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')) ?>

130 | P a g e

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

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

The last option is to simply force the use of a specific blog post template:

131 | P a g e

<?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

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.

132 | P a g e
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.

133 | P a g e
(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.)

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:


disqus_enabled: true

disqus_shortname: yourdisqusshortname

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're using the 1.4 version of apostrophe, you will need to override a few blog templates to
enable disqus.

Here's how to set up Disqus with the Apostrophe 1.4 blog plugin:

1. [http://disqus.com/ Visit Disqus and set up an account].

134 | P a g e
2. On the "Choose Install Instructions" page, click "Universal Code."

3. 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 ?>


Should become:


<?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) ?>



135 | P a g e
To this:


<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>



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`:

136 | P a g e

<script type="text/javascript">

var disqus_developer = 1; // this would set it to developer mode



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:




# 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.

137 | P a g e
= Apostrophe Manual =

[ManualDevelopersGuide Back to the Developer's Guide]

[ManualOverview Up to the Overview]

== Internationalization ==

Apostrophe supports internationalization and localization of sites in two critical ways: translation of
the administrative interface, which your clients see, and translation of the actual site content, which
end users of the site will see.

This section assumes familiarity with earlier sections of the manual (the Developer's Guide is

=== What Languages Are Available? ===

Apostrophe supports UTF-8, so you can potentially translate your content into any language. At this
time the user interface used by those editing content on the site supports French, Spanish, German
and English, with more languages on the way. If you are editing site content in a language for which
our administrative interface has not yet been translated, you'll see an administrative interface in

=== UTF-8 and Apostrophe ===

Apostrophe stores content in UTF-8 for compatibility with nearly all languages. However, the
Apostrophe 1.0.x series has issues with generating slugs for items with non-Latin1 characters in their
names. Support for this is already committed in the trunk and will be standard in the 1.1 series of
Apostrophe. So those who wish to use Greek and other non-Latin character sets today should use
the svn trunk of the plugin and migrate to the 1.1 stable branch when it appears.

138 | P a g e
In any case, for good results with non-ASCII characters you must ensure that your MySQL database is
configured to store and collate information in UTF-8. See the `databases.yml.sample` file provided
with Apostrophe for the required settings. Note the attribute settings below:




class: sfDoctrineDatabase


dsn: mysql:dbname=demo;host=localhost

username: root

password: root

encoding: utf8




DEFAULT_TABLE_COLLATE: utf8_general_ci


If you already have a database with the Latin1 character encoding or collation you will need to use
ALTER TABLE statements to migrate it to UTF-8. See the MySQL documentation for more information
on this issue.

To use UTF8 characters reliably in URLs with Symfony you must also modify your `frontend_dev.php`
and `index.php` controllers to ignore `PATH_INFO`. Apache helpfully decodes UTF-8 URLs in
`PATH_INFO`, unfortunately trashing them in the process. However Symfony is perfectly capable of
getting by with just the `REQUEST_URI` environment variable, which it uses when there is no explicit
PHP script name in the URL. So add this code to the top of your front end controllers (after the `<?`
line of course) and you'll be ready to go with non-Latin URLs:


139 | P a g e


=== Allowing Users to Switch Languages ===

By default, there is no user interface to switch languages in Apostrophe. You can turn this on easily
with two `app.yml` settings: one to turn on the user interface and another to specify which
languages are available. A third setting is required to fully internationalize search indexing, a step
you may wish to skip if your site is used only in Latin languages because Zend Search is not able to be
clever in its handling of plural versus singular words and the like when in strict UTF-8 mode.

Here is an example. Note that these steps have already been carried out in our current sandbox




# If true, there will be a language switcher next to the login/logout button

i18n_switch: true

i18n_languages: [en, fr, de, es]


You must also turn on support for internationalization in `settings.yml`, and may change the default
language from English to another language there as well:




i18n: on

default_culture: en

140 | P a g e

Be sure to `symfony cc` after this and any change to `app.yml`.

Then copy the `apps/frontend/i18n` folder from our sandbox project to your own project. This folder
contains the `apostrophe.fr.xml`, `apostrophe.de.xml`, etc. files used to provide translation on the

The language switcher can be used by both end users of the site and client staff who need to edit the
site in a user interface that speaks their preferred language.

(Developers: if you don't care for the user interface of the language switcher, consider overriding the
`a/login` partial at the application level. It is also a good candidate for progressive enhancement via

When you switch languages you will immediately notice that the home page has no content. Is this a
bug? Not at all. The content has not been translated into the new language yet. That's the topic of
the next section.

=== Translating Your Site Content ===

When you switch languages, you find yourself looking at a blank page. That's because the content
has not yet been written for that page in that language. Just start adding slots!

Of course, you might want to use the content in another language as a starting point. We agree that
this could be more convenient, however a handy workaround is to open a separate browser (Firefox
if you use Chrome, and vice versa) and leave that browser logged into that site in the original
language for reference. We'll be investigating ways to make this process easier, but the two-
browsers method works very well in practice.

=== Adding the Culture to the URL ===

141 | P a g e
In order for Google to index content in several languages you will need distinct URLs for several
languages. You can do this by changing your `a_page` route (beginning with Apostrophe 1.5):



url: /:sf_culture/:slug

param: { module: a, action: show }

requirements: { slug: .* }


This allows Google to index separate pages for separate languages and allows links to language-
specific pages to be shared by users.

This leads to the question, "how does Google ever see the home page in a language other than
English?" Our default language switcher just changes the user's culture and redirects to the home
page without a culture in the URL. But you can also access culture-specific homepages at `/de/`,
`/en/`, `/fr/` etc.

Consider removing the standard "homepage" route and replacing it with an action at the project
level that redirects to the home page URL for the default language. Also consider adding direct links
to the homepages for the other languages you support on your site. This will allow Google to find

=== Translating the User Interface Into a New Language ===

As with any Symfony project, internationalization of the user interface is accomplished via XLIFF files.
And you can find complete translations in the `apps/frontend/i18n` folder of our sandbox project for
several languages, including French, German, and Spanish. Install the sandbox project to see these in
action right away. You can of course copy them to your own project's `apps/frontend/i18n` folder.

To add a new translation, you can take two approaches:

142 | P a g e
1. [http://groups.google.com/group/apostrophenow Join the Apostrophe Now Google Group] and
express interest in translating the user interface. Our team will give you access to a convenient back
end interface for translating the content, and your work will benefit the entire Apostrophe open
source community.

2. Alternatively, just copy one of the existing XLIFF files in `apps/frontend/i18n` in our sandbox
project, changing the language code to the appropriate 2-letter ISO code for your language, and get
started translating. Those who are comfortable with XLIFF files may prefer this approach. Of course,
we hope you will decide to share the results with the rest of the community.

[wiki:ManualImportGuide Continue to Import & Migration]

[ManualOverview Up to Overview]

143 | P a g e
= Apostrophe Manual =

[wiki:ManualI18N Back to the Internationalization Guide]

[ManualOverview Up to the Overview]

Apostrophe includes an import task that is capable of importing a site from a valid xml document.
The task can be run using


./symfony apostrophe:import-site


The task expects xml files to be located in sfRoot/data/a

A sample document is shown below.


<?xml version="1.0" encoding="UTF-8" ?>


<Page slug="/" title="Home" template="home" >

<Area name="body">

<Slot type="aRichText">

<value>This is my body text</value>



<Area name="header_image">

144 | P a g e
<Slot type="aImage">

<MediaItem src="header.jpg" alt="Header"/>



<Page slug="/about" title="About" file-id="2" template="default" />

<Page slug="/people" title="People" file-id="3" template="default" >

<Page slug="/people/students" title="Students" id="4" template="default" />

<Page slug="/people/teachers" title="Teachers" id="4" template="default" />





An example of an external file used to include slot info.



<?xml version="1.0" encoding="UTF-8"?>


<Area name="body">

<Slot type="foreignHtml">


<b>This is blod text</b><img src="food-fast.gif">More regular text. <a

src="http://farm5.static.flickr.com/4090/5073384290_63ea8ab19d_b.jpg"></a> And another




145 | P a g e



Page elements have 4 attributes, currently both the slug and title are required. Template is optional
and will default to default. The file-id is used to specify an external file where information about
areas and slots can be found, this is useful for particularly large sites where loading one XML file is
not feasible. A file-id of 2 will result in the importer looking for the file data/pages/2.xml. If file-id is
not specified the importer will not look for areas in the main xml file.

Currently 3 slot types are supported by the importer: aRichText, aImage, and foreignHtml. '''The
foreignHtml content type is used to import rich text with embedded images''', the import process
will create multiple rich text and imageSlots.

MediaItem src attribute can either be a http:// format url or a relative url. If relative importer will
expect file to be located at data/a/images.

xsd schema is attached to this page.

[ManualOverview Up to the Overview]

146 | P a g e
The following is our internal process for deploying new Apostrophe client sites. We share it with you
to better document useful tools like `apostrophe:deploy` and `project:sync-content`. Please note
that it is not mandatory at all to use those tools with Apostrophe. But we like and recommend them
and also feel this document would benefit from third-party feedback.

- The team at P'unk Avenue

== P'unk Avenue Deployment Process ==

==== A guide for client system administrators and other technical staff ====

Our process is to develop sites on Macs, using svn for version control and local copies of Apache and
PHP to interact with the site before it is deployed. Completed code is then deployed via rsync to a
staging server for approval by the client and then deployed from there to the actual production
server. In some cases a staging server is not available, in which case we deploy directly to
production, although our preference is naturally to use a staging server.

Note that we do not use svn for final deployment. Instead we use svn to keep the code up to date on
development Macs and use various Symfony tasks (based on rsync, mysqldump and related
commands) to deploy code and content as needed.

So deploy the site to production, it is necessary to sync the code first from a development Mac to
the staging server and then on to the production server. This assures that each change has been
properly reviewed. It also means that "hotfixes" are never made directly to a server and such
changes will be overwritten by future deployments that do follow the process. Contributing directly
at the code level requires svn access to the project.

=== Setting Up config/databases.yml and config/properties.ini for production use ===

The `config/databases.yml` file must have database settings for the production server as well as for
the staging server. Examine this file to see how the staging settings are set up (it is a human-
readable configuration file; take care to preserve the indentation as it is significant). If you prefer, for
security reasons, not to keep this file in svn where developers can see it, we can take it off the list of
files that are synced to to the production server, and you can manually maintain a special production

147 | P a g e
copy there. This is common on sites where P'unk Avenue does not have direct access to the
production server.

PLEASE NOTE: do not make direct changes to the database without consulting with the rest of the
developers involved. In particular the various tables that make up the Apostrophe content
management model should be manipulated through Apostrophe's Doctrine-based model layer and
never directly with SQL statements. Otherwise the page tree and versioning system can be easily
damaged. There are rare exceptions but it is best to discuss them with P'unk Avenue first.

The `config/properties.ini` file must contain ssh credentials (but no password) for the production
server. Examine this file to see how the staging settings are set up. Since this file does not contain
the password there is no security risk in adding the production settings to this file.

=== Syncing For The First Time ===

When syncing code to the production server for the first time, you will receive some errors when
running the `apostrophe:deploy` task. This is normal and due to the fact that the `web/index.php`
file has not been set up on that server yet. This file is not synced because it is specific to each server.
(Note to experienced Symfony developers: this is a departure from the default "frontend_dev.php"
approach that we prefer because it simplifies debugging and deployment. `index.php` acts as the
sole "switch" that determines which settings should be used on a particular host.)

After the first code sync, copy `web/index.php` from staging to production manually, and edit the
file so that it enables the prod environment rather than the staging environment:


ProjectConfiguration::getApplicationConfiguration('frontend', 'staging', true);


=== Syncing Code ===

To sync code from one server to another, we use the `apostrophe:deploy` Symfony task. The
following command will push the current code from the staging server or a development Mac to the

148 | P a g e
production server. Note that this does not affect the content on the site, only the code. This is the
way to deploy fixes to the code, or to initially deploy the site to production:


./symfony apostrophe:deploy production prod


You will be prompted several times for the ssh password. This command can take a long time and
sometimes runs quietly. It carries out several Symfony commands:











Also, the `project:deploy` task is given specific rsync arguments that ensure rsync is not fooled by
overlapping timestamps if two different developers have deployed recently. This is important to
avoid surprising results especially with the APC cache.

Note that the `apostrophe:fix-remote-permissions` task uses a password set in `config/properties.ini`

to request that the remote website adjust permissions on files that must be writable by both
command line tasks and the webserver. The relevant section of `properties.ini` is:


149 | P a g e



Permissions and deployment are a source of great frustration in Symfony development: files created
by Apache are usually not writable by cron jobs and vice versa. If everything is in the database you're
OK, but if you need to manage files you have a problem on your hands. '''The best way to address
this issue is to run command line tasks and Apache as the same user''', which does not introduce any
great new security risk, since if PHP is compromised it is still possible to call `system()` even when
the Apache user has no shell. However, if you are not comfortable with this, `apostrophe:fix-remote-
permissions` is a useful workaround. it invokes the `aSync/fixPermissions` action, which carries out
the same steps as the `project:permissions` task, but does so as Apache.

To deploy new code from a development Mac to the staging server (which we recommend doing
first before deploying anything to the production server), use this command:


./symfony apostrophe:deploy staging staging


You can then run:


./symfony apostrophe:deploy production prod


Directly on the staging server to deploy the final step to production after the client has approved the
changes. If P'unk Avenue does not have direct access to the production server then this command is
carried out by the client system administrator.

=== Syncing Content ===

150 | P a g e
Pushing code to the production server is normal. But pushing content to another server should be
done with great care and caution. Always think about what machine you are syncing from and what
machine you are syncing to. There is NO way to undo this operation, unless proper backups of the
database and the web/uploads folder are being made. Measure twice, cut once.

==== One-Time Content Sync TO Production At Launch ====

The following command, typed on the staging server, will sync content FROM the staging server TO
the production server. This should only be done ONCE when the production server is first set up, and
then once more after content has been frozen on staging in anticipation of launch:

ALMOST CERTAINLY A BAD IDEA (except the very first time production is set up):


./symfony project:sync-content frontend staging to prod@production


You will be prompted several times for the ssh password. This command can take a long time and
sometimes runs quietly.

Note that the `project:sync-content` task copies both the MySQL database and the `web/uploads`
and `data/a_writable` folders, as well as any other data folders specified in `app.yml` (usually these
are the only ones).

==== Periodic Sync Back From Production for Better Testing ====

The following command, typed on the staging server, will sync content FROM the production server
back down TO the staging server. This command is quite useful for making sure the staging server’s
content is a realistic test of what will happen when new code changes are eventually pushed to
production. Content "lives" in production (after launch), so it makes sense to periodically refresh the
content on staging with the current content of production:

151 | P a g e

./symfony project:sync-content frontend staging from prod@production


Note the use of “from” rather than “to” above. Always proofread this command carefully.

The following command, typed on a development Mac, will sync content FROM the staging server
back down TO the development Mac for realistic testing:


./symfony project:sync-content frontend dev from staging@staging


=== After Syncing Content: Rebuilding the Search Index ===

After syncing content TO production, you would run this command ON production to rebuild the
search index:


./symfony apostrophe:rebuild-search-index --env=prod


Similarly, you would run this command on staging and use `--env=staging` if you synced content back
down to staging. Otherwise searches will not return results.

Rebuilding the search index on a development Mac is an optional step if you are not testing search-
related issues.

152 | P a g e
Rebuilding the search index does not take the site down in the meantime.

=== Frequently Asked Questions ===

“Why ‘production prod’ and not just production?” It’s possible for Symfony sites to have several
environments on one server although we don’t recommend that practice or use it on our client

“Why frontend?” Symfony projects can contain several sub-applications. Our projects typically
contain only one "application" because we believe in progressively enhancing the user's experience
to include admin features rather than creating a typically less user-friendly "back end" application
that is often neglected in the design process.

"I made a change to the code on the staging or production server and someone deployed and now
the change is gone. How do I make changes that stick?" You need svn access to the project so you
can participate in version control and avoid conflicts with other developers on the project. Code
changes are typically made on a development laptop and then committed with 'svn commit,' never
by hotfixing files on servers.

=== Further Reading ===

Complete developer documentation for Apostrophe, our content management system, is available
in the [ManualOverview Apostrophe manual]. For client technical staff interested in contributing at a
designer or developer level, that is the right place to start reading.

153 | P a g e