= 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 fullpage 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 framework.

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.symfonyproject.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 designbusting 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]. 4|Page

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 memory * Optional, for previews of PDFs in PDF slots: ghostscript (you must also have netpbm to use this feature) * 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


Mac users: want to build your site on your own computer first? Please say yes. If you are using Red Hat Enterprise Linux or CentOS. Ubuntu includes a sufficiently modern version of PHP right out of the box. 7|Page . we recommend Ubuntu.php` page that verifies these requirements is included in the sandbox.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.2. you will need to upgrade PHP to version 5. Just visit that page after adding the `web` folder of the sandbox to your server as the document root of a new site. add this line to the `.mamp.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.info/ MAMP]. To fix that. Yes. Core Apostrophe features do not depend on Unix-specific command line tools and services. this is the right way to avoid surprises. Note that MAMP's PHP must be your command line version of PHP. If you are choosing a Linux distribution.3 series. You can most easily meet the web server and PHP requirements by installing the latest version of [http://www. you can also use Apostrophe with Microsoft Windows as a hosting environment.netpbm and ghostscript with a few simple commands like sudo apt-get install netpbm and sudo aptget install ghostscript. This is unfortunate and Red Hat really ought to get a move on and fix it. not Apple's default install of PHP. We strongly recommend that you use the Microsoft Web Platform PHP accelerator for reasonable performance on Windows. Apple's default version of PHP for Snow Leopard is theoretically capable of working with Apostrophe.x on your own. A `servercheck. but unfortunately Apple chose to ship the very first bleeding-edge version of PHP 5.

apostrophenow.com/downloads/asandbox-stable. you can check out the trunk: {{{ svn co http://svn.=== Ways to Download the Apostrophe Sandbox Project === ==== Download The Tarball ==== The easiest way is to just [http://www. We use the stable branch for our own client projects. which is suitable for use with both Symfony 1. ==== Check It Out From Subversion ==== Alternatively you can check it out with subversion: {{{ svn co http://svn.4.apostrophenow.apostrophenow.4 stable branch. but note that you won't be able to get security and stability fixes just by typing `svn update` this way.] This tarball is updated nightly from the stable branch of our sandbox. because it results in more feedback for us.org/sandboxes/asandbox/trunk asandbox }}} We love it when you do this. This is a great way to get started.3 and Symfony 1.4 branch instead''').org/sandboxes/asandbox/branches/1.gz download a tarball of the sandbox project. 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 asandbox }}} That will check out the 1. but you should definitely consider using the stable branch for your client projects.tar. 8|Page .

In this case I have already copied the `svnforeigncopy` script to my personal `~/bin` folder and made it executable with the `chmod` command. but you'll soon wonder how to save your own changes when the project is part of our own svn repository. Here's an example.net/projects/svnforeigncopy/ svnforeigncopy] each time you want to start a new site. If you don't have svn 1. with the `svn:ignore` and `svn:externals` properties completely intact. it might be better to download the tarball. That's what we do. This will give you all of the necessary plugins and the ability to `svn update` the whole shebang with one command.5 or better. The important thing is that you are still connected to the latest bug-fix updates for our plugin.5 or better. So does Ubuntu Linux. which you can adjust in `~/. that's really not a big deal. but since 99% of the code is in the externally referenced plugins and libraries. You don't get the project history. just install our tarball instead. such as a beanstalk repository. of course you can use an existing one. MacOS X Snow Leopard includes a 1.) ```NOTE:``` You need svn version 1. For more information about svn see the [http://svnbook. That's why we recommend that you instead copy it to your own repository with [https://sourceforge.6.com/ official svn red bean book]. don't worry about it. (That assumes `~/bin` is in your PATH.red-bean. If this is all Greek to you.==== Our Favorite: Copying the Sandbox to Your Own Repository ==== Checking out the sandbox from svn is a nice way to get started. Of course you should substitute your own preferred location for `/Users/boutell` here: {{{ svnadmin create /Users/boutell/myrepo }}} 9|Page .profile` or ~/.x version of svn. This command creates a new `svn` repository. which is plenty good enough. With `svnforeigncopy` you get a copy of the asandbox project in your own svn repository.bashrc`.

and the local checkout folder where you'll work with the end result (also used as a scratchpad during the copy operation). and the checkout of symfony in lib/vendor 10 | P a g e . edit `/Users/boutell/myrepo/db/fsfs.apostrophenow. the path you're copying to.4 file:////Users/boutell/myrepo/demosite Sites/demosite }}} Those arguments are the path you're copying from. this command copies the project from our repository to yours: {{{ svnforeigncopy http://svn. 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). after you create your repository (see above). Unfortunately it is turned on by default in svn on MacOS X Snow Leopard. Now.conf` and add this line: {{{ enable-rep-sharing = false }}} This disables an optional optimization that can cause problems.```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.org/sandboxes/asandbox/branches/1. Thanks to Pablo Godel of !ServerGrove for pointing out the issue.

In most web hosting setups.sample` for tips on virtual host configuration for Apache.info/ MAMP] if you are on a Mac. or by &. `httpd.means it'll take a little while to do the final 'update' that brings down the related plugins and Symfony itself. though.ini` file: 11 | P a g e .org/en/xampp. Apostrophe currently requires that they be separated by &.mamp. As with any Symfony project you'll want to allow full Apache overrides in this folder. you're ready to configure your testing web server to recognize a new website.output & }}} If PHP directives are not honored in `.htaccess` of your Apostrophe project to ensure that the separator is &: {{{ php_value arg_separator.conf` or whatever your web hosting environment requires. 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.htaccess` will be honored by your server. MySQL. Light up the folder `asandbox/web` as a website named `asandbox` or whatever suits your needs via MAMP. === Apache Configuration === Once you have fetched the code by your means of choice. These are allin-one packages with PHP. Apache and everything else you need to test real websites on your own computer. you can add this line to `web/. 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. you can do this in your `php. Make sure the directives in `web/. You can also install Apostrophe directly on a web hosting account.ini` setting that determines whether parameters in URLs are separated by &amp.html XAMPP]. When it's over. See `config/vhost. Windows and Linux users should consider using [http://www.htaccess` files in your hosting setup.) === PHP Configuration === There is a `php. (Confused about this MAMP stuff? Wondering how you can test websites on your own computer? You really should pick up [http://www.apachefriends.

=== Symfony Configuration === Yes. 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.yml.output = "&" }}} You must also have a reasonable PHP `memory_limit` setting. Now create the `config/databases.{{{ arg_separator.yml 12 | P a g e .yml. Note that without this rule content editing will not work properly in Apostrophe.sample config/databases. Don't forget to restart Apache if you make changes in `php.yml` file.ini`. which must contain database settings appropriate to ''your'' system. this is a preconfigured sandbox project.sample` as a starting point: {{{ cp config/databases. but you do have to adjust a few things to reflect reality on your own computer. Copy the file `config/databases.

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.ini file just defines the name of the project. The sample properties. copy config/require-core. cd to the `asandbox` folder and run these commands. in addition to the name of the project. `apostrophe:deploy`.}}} If you are testing with MAMP the default settings (username root. such as a shared install for many sites (note that only 1.4.x and 1.php and edit the paths in that file. If you want to use a different installation of Symfony. Next.x stable branch that is included in the project. password root. If you are testing on a staging server you will need to change these credentials.ini }}} In Symfony `properties. Also create your `properties. 13 | P a g e .x are likely to work).4. At this point you're ready to use the checkout of Symfony's 1.ini` file: {{{ cp config/properties.3.php.example to config/require-core.ini` contains information about hosts that a project can be synced to.sample config/properties. database name asandbox) may work just fine for you.ini. See the Symfony documentation for more information about that technique.

These are the subfolders in `web` that end in `Plugin`. Note that svn does NOT store permissions so you can NOT assume they are already correct: {{{ ./symfony doctrine:data-load }}} This will create a sample database from the fixtures files.) {{{ ./symfony project:permissions 14 | P a g e . Unix/Linux users don't have to worry about this./symfony plugin:publish-assets . you can pull down our full demo site as an alternative to the somewhat bland fixtures site./symfony apostrophe:demo-fixtures }}} Note that this task will run for quite a while as media files are included in the download. folders first. If you prefer.(Windows users: you should remove the `web/apostrophePlugin`. Now set the permissions of data folders so that they are writable by the web server. etc. `web/apostropheBlogPlugin`./symfony cc ./symfony doctrine:build --all . Replace the `doctrine:data-load` command with this one (it's OK to do this if you already did the other command): {{{ .

search is included). 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. cache and log folders. build the site's search index for the first time (yes./symfony apostrophe:rebuild-search-index --env=dev 15 | P a g e .symfony-project. After this. So before criticizing the "777 approach. Handy. 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 sfSyncContentPlugin: {{{ .}}} Our apostrophePlugin extends project:permissions for you to include the data/writable folder in addition to the standard web/uploads." be sure to [http://trac. Production Symfony sites should run on a virtual machine of their own. Next. or share a VM only with other sites written by you.org/wiki/SharedHostingNotSecure read this article on shared hosting and Symfony]. It doesn't live in the database so it needs to be done separately. However be aware that most "shared hosting" environments are inherently insecure for a variety of reasons.

the password will be `demo`). Start adding subpages. without starting from the asandbox project.x has undocumented backwards compatibility breaks and no documentation of its new features . the login is `admin` and the password is `demo`.0. }}} === 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.version 5.org/plugins/apostrophePlugin apostrophePlugin] 16 | P a g e . adding slots to the multiple slot content area.) '''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.. Begin by installing the following Symfony plugins into your Symfony 1. have a ball with it! {{{ Note: For all versions of the demo site.. editing slots.x .we cannot recommend it right now) * sfDoctrineActAsTaggablePlugin * sfWebBrowserPlugin * sfFeed2Plugin * sfSyncContentPlugin (recommended but not required) * And of course.symfony-project. The notes that follow assume you're doing it the hard way.4 project: * sfJqueryReloadedPlugin * sfDoctrineGuardPlugin (4.0. You'll want that later when working on a production server. [http://www.3/1.}}} (You can specify `staging` or `prod` instead to build the search indexes for environments by those names.

If you are using svn externals to fetch plugins.] If you choose to install this system-wide where all PHP code can easily find it with a `require` statement. you'll need to modify your `ProjectConfiguration` class 17 | P a g e .We strongly encourage you to do so using svn externals. [http://framework.. so you must also install the Zend framework.''' The 5./plugins/apostrophePlugin/web apostrophePlugin # Similar for other plugins required }}} The search features of the plugin rely on Zend Search. So we cannot recommend it yet. but there is no documentation of the many changes. great.3 branch: `http://svn.x series. or create your own links with a relative path: {{{ cd web ln -s . pin your `svn:external` for sfDoctrineGuardPlugin to the 1. If you prefer to install it in your Symfony project's `lib/vendor` folder.x series has been declared stable.symfony-project.0.zend.3` Also. Use the `plugin:publish-assets` task.com/download/latest The latest version of the minimal Zend framework is sufficient.com/plugins/sfDoctrineGuardPlugin/branches/1.0. 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. including backwards compatibility breaks in the names of relations. '''For sfDoctrineGuardPlugin you currently must use the 4. This is not what `plugin:install` gives you by default.

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 set_include_path( sfConfig::get('sf_lib_dir') . // for compatibility / remove and enable only the plugins you want $this->enableAllPluginsExcept(array('sfPropelPlugin')). PATH_SEPARATOR . 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 . '/vendor' . get_include_path()). } } }}} Create an application in your project.

If you don't want to display the login link (for instance.yml: {{{ all: sfShibboleth: domain: duke. you'll want to change these actions in apps/frontend/config/app.edu a: actions_logout: "sfShibbolethAuth/logout" actions_login: "sfShibbolethAuth/login" }}} You can also log in by going directly to `/login`.The CMS provides convenient login and logout links.yml` file. because your site is edited only you). By default these are mapped to sfGuardAuth's signin and signout actions. Of course you may need other modules as well based on your application's needs: 19 | P a g e . just shut that feature off: {{{ all: a: login_link: false }}} You will also need to enable the Apostrophe modules in your application's `settings. If you are using sfShibbolethPlugin to extend sfDoctrineGuardPlugin.

{{{ enabled_modules: -a .aSync .aFeedSlot .aAdmin }}} 20 | P a g e .aImageSlot .aPDFSlot .aRichTextSlot .aButtonSlot .sfGuardPermission .default .aUserAdmin .aPermissionAdmin .aMediaBackend .aMedia .aNavigation .aTextSlot .taggableComplete .aSlideshowSlot .aGroupAdmin .aRawHTMLSlot .sfGuardAuth .aVideoSlot .aNavigation .

A recent version of FCK is included with the plugin. addressing this issue in validators.yml. However you'll need to enable FCK in your settings. load the fixtures for a basic site. otherwise Apostrophe slots will not work properly: {{{ all: # 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.yml file. Every site begins with a home page with all other pages being added as descendants of the home page: {{{ 21 | P a g e . we will. Apostrophe's strategy is to store valid UTF-8 encoded HTML to begin with.) Now.4 project escapes all output that is emitted in templates. a new Symfony 1. as follows: {{{ all: rich_text_fck_js_dir: apostrophePlugin/js/fckeditor }}} ==== Disabling Output Escaping ==== By default. Currently you must disable output escaping in settings. If we can do that without too much overhead.Apostrophe edits rich text content via the FCK editor.

. depending on your needs. Then look in data/sql/schema.symfony-project.yml }}} In particular you must have a home page and the hidden `/admin` and `/admin/media` engine pages./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: {{{ .org/ticket/7272 Symfony bug 7272 ] for more information and patches if you are interested in pursuing that approach. Unfortunately there is currently a bug in Symfony and Doctrine that prevents successful use of this feature with tables that use inheritance. You can do so without clobbering your existing database: {{{ . See [http://trac./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. 22 | P a g e ./symfony doctrine:build --all .sql for the CREATE TABLE statements for tables beginning with the `a_` prefix. You will also want to load the fixtures for the apostrophePlugin or. Ideally you would use Doctrine's migration schema diff task instead./symfony doctrine:data-load --append plugins/apostrophePlugin/data/fixtures/seed_data. It's probably easier to just locate the CREATE TABLE statements. edit them to suit your purposes better first.

but before any other rules.+$ RewriteCond %{REQUEST_FILENAME} -f RewriteRule .[L] # If it doesn't exist. {{{ ###### BEGIN special handling for the media module's cached scaled images # If it exists.L] ###### END special handling for the media module's cached scaled images }}} ==== Routing Rules ==== 23 | P a g e .htaccess file after the `RewriteBase /` rule.Also see the fixtures provided with the sandbox project for strongly recommended (but not mandatory) permissions and groups settings. 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. These should be copied to your .*)$ index.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.php [QSA.htaccess rules are required to enable this. just deliver it RewriteCond %{REQUEST_URI} ^/uploads/media_items/.* . The following special . if you are using one. ==== .+$ RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(. render it via the front end controller RewriteCond %{REQUEST_URI} ^/uploads/media_items/.

}}} {{{ default: url: /admin/:module/:action/* }}} 24 | P a g e . At some point we'll make this reserved prefix more # configurable.By default Apostrophe will map CMS pages to URLs beginning with `/cms`: {{{ /cms/pagename }}} And leave all other URLs alone.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. If the CMS is the main purpose of your site.yml: {{{ a: routes_register: false }}} And register these as the LAST rules in your application's `routing. This is appropriate if the CMS is a minor part of your site. shut off the automatic registration of the route above in app.

{{{ # A homepage rule is expected by a and various other plugins. # so be sure to have one }}} {{{ homepage: url: / param: { module: a. action: show. # before the catch-all rule that routes URLs to the # CMS by default. slug: / } }}} {{{ # Put any routing rules for other modules and actions HERE. action: show } requirements: { slug: .* } }}} 25 | P a g e . }}} {{{ # Must be the last rule }}} {{{ a_page: url: /:slug param: { module: a.

Take a close look at `modules/a/templates/layout. '''You can change the a_page rule. If your application layout does not offer such a slot to override. If you experience missing bits of the UI with your own layout. including the built-in media repository functionality and media slots.''' ==== 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. expect a layout that offers certain overridable Symfony slots. the user admin feature and the media repository.0.7 ** the media repository in particular defaults to using a built-in layout that is not really suitable for its current markup. 26 | P a g e .7 this is no longer necessary. the media repository will be missing some of its interface. such as the "reorganize" feature. will not work. ** Prior to Apostrophe 1.yml` setting which is also found in the sandbox project: {{{ all: aMedia: use_bundled_layout: false }}} Beginning in Apostrophe 1.0. Otherwise Apostrophe engines. but you must have such a rule and it must be named `a_page`.Thanks to Stephen Ostrow for his help with these rules. prefixing the page slug with anything you wish. The media repository overrides this slot to offer media navigation links instead of a list of subpages.php Requirements ==== Apostrophe's non-CMS pages. make sure you offer the same slots at appropriate points. You can fix this with an `app. in particular the `asubnav` slot.

This folder is used for search indexes and other data that does not live in the database. Previously a separate setting was provided to specify that your site has many pages. even if your site has thousands of pages. ==== Set Permissions ==== In Symfony. In fact in Apostrophe 1.1 we will be migrating toward moving more of the hard-to-override. 27 | P a g e ./symfony project:permissions }}} ==== Performance Optimization ==== Sites with many pages sometimes require some tuning for good performance. This folder is used for files that should be writable by the web server but not directly accessible via a URL. the `cache` and `web/uploads` folders are always world-writable so that the web server can store files. You can set up this folder yourself. which apostrophePlugin enhances to handle this folder: {{{ .==== Styling Requirements ==== The user interface will be essentially complete at this point. site-design-specific CSS from `a. `data/a_writable`. So you may want to refer to that file as a reference.css` file of the sandbox project. but this setting has been removed as we've found the code for the "worst case" scenario is acceptably fast on small sites too. Apostrophe is configured to provide good navigation and reasonable performance without running out of memory. By default.css` file or apply some styles of your own. but it's more convenient to use Symfony's `project:permissions` task. 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` to the `main. Apostrophe adds one more such folder.

because some results may be locked pages the user is not eligible to see.yml` option: {{{ all: a: 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. You can address this by setting a hard limit on the number of pages Apostrophe should consider when generating search results. otherwise it does not save memory./symfony cc }}} ==== Scheduled Tasks: Enabling Linked Accounts ==== 28 | P a g e . Searches for common words can exhaust the memory available.However. ==== 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 point: {{{ . That filtering takes place after the hard limit. Note that this is not the same thing as the number of results the user will see. set this `app. To set a hard limit on the memory required for a search. So the limit should be set generously. there is an issue with searches on sites with thousands of pages.

We suggest using a cron job to run this task every 20 minutes./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.5 and above:''' if you wish to take advantage of the "Linked Accounts" feature.'''Version 1. you will need to schedule the following command line task to run on a scheduled basis. Change the `-env` setting as appropriate to your environment: {{{ . which automatically brings new media into the media repository from Youtube and other media services specified by the site administrator. [ManualEditorsGuide Continue to the Editor's Guide] [ManualOverview Up to the Overview] 29 | P a g e .briefly because it is so much more fun to simply use it.

sometimes a major change does justify a few manual steps to allow new features to be implemented. just as we did during the transition from `pkContextCMS` to `apostrophePlugin`. 30 | P a g e . For instance. However. more specifically for the open source Symfony plugins `apostrophePlugin` and `apostropheBlogPlugin`.2. If you used `plugin:install` you should be able to use `plugin:upgrade` to install the latest version of `apostrophePlugin`. For the most part we avoid unnecessary schema changes but sometimes additions are necessary to deliver new features. * Always run the `symfony cc` task after upgrading. * Always run the `apostrophe:migrate` task after upgrading./symfony apostrophe:migrate --env=dev` This task upgrades database tables as needed. Remember that you should upgrade the blog plugin at the same time. on your development computer.4. then of course you'll need to download the new version. We'll add notes on these issues to this page over time to ensure that an upgrade path is available. you would type: `. We generally avoid these in patchlevel releases (1. For the more part this process is all but automatic within the Apostrophe 1. === Standard Upgrade Steps === * If you are installing Apostrophe via the `plugin:install` task or downloading Symfony plugin tarballs directly from the Symfony plugins site.= Apostrophe Manual = [ManualOverview Up to the Overview] == Upgrading Apostrophe == === Overview === From time to time we issue upgrades for Apostrophe.x series. If the blog plugin is present it will also upgrade its database tables.

Enable the specific plugins you want. Otherwise certain class files will not be found beginning in version 1.class.3.''' Do NOT use `enableAllPluginsExcept` for this purpose.. 1. Beginning in version 1.5). If you are not using MySQL. However the source code of the task remains a valuable resource for determining the necessary changes. then the `apostrophe:migrate` task will not work directly for you.5 ==== '''First make sure that apostropheBlogPlugin is enabled AFTER apostrophePlugin in your config/ProjectConfiguration. 'permid' => $permid. either on your own or by using the `apostrophe:generate-slot-type` task.5. you will need to make a small change to the `normalView` partial of your slot.1. They are more common in minor version changes (1. with the blog plugin listed after the main Apostrophe plugin.. === Version-Specific Upgrade Notes === Be sure to consult these when migrating from an older version up to or beyond the version mentioned.) but they may be necessary to address bugs in rare cases.4. For example: {{{ <?php include_partial('a/simpleEditButton'. array('pageid' => $page->id. 'name' => $name. the `slot` parameter must be added to the list of parameters to the `simpleEditButton` partial.php file. ==== Version 1.4. 'slot' => $slot)) ?> }}} 31 | P a g e . Developers: if you have created custom slot types.5.

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

Click "log in" and log in as the admin user (username admin. and users who do not have editing privileges on the page. they are able to make the following changes by clicking the "This Page" button. maintaining and administering the content of the site. But users with suitable editing privileges will see the page with its title 33 | P a g e . Access your new Apostrophe site's URL to see the home page. Anonymous users." then the "gear" icon for less frequent changes Apostrophe emphasizes "unpublishing" pages as the preferred way of "almost" deleting them because it is not permanent. 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. will see the usual 404 Not Found error. This greatly reduces the learning curve for editors. All of these tasks are performed through your web browser. === Managing Pages === When a user has appropriate privileges on a page.= Apostrophe Manual = [ManualOverview Up to the Overview] == Editor's Guide == This section is devoted to users who will participate in editing. Notice that editing controls are added to the normal experience of the site. password demo) to see the editing controls. No command line skills are required.

you'll note that each editable slot has an "Edit" button or. which is discussed in more detail later. video. in the case of the media slots. such as Twitter feeds. PDF. 34 | P a g e . 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. === 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_). button) * Custom slots (of any type. including itself.) You can do the same thing with the tabs at the top of the page. but it is useful when you create an unnecessary page by accident. slideshow. "Select Image" and similar buttons. inserted into a page) * Raw HTML slots (best avoided in good designs. You can also delete a page permanently via the small X in the lower right corner of the page settings dialog. the side navigation displays its peers instead."struck through" and will be able to undelete the page if they desire. (If a page has no children. Also check out the "Reorganize" button. but useful when you must paste raw embed codes) * Media slots (image. implemented as described in the developer's guide section) Once you have logged in. This prevents the loss of content. Most of the time that's a shortsighted thing to do. 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.

" then "Image. Just click "History. The arrow-in-a-circle icon accesses a dropdown list of all changes that have been made to that slot. === Revising History === Did you make a mistake? Not a problem! Both slots and areas allow you to roll back to any previous edit. 35 | P a g e . This media repository provides a clean way to keep track of the media you have available. Media slots provide a robust way to manage photos and videos without wrecking the layout of the site." then "Select Image.Every slot also offers version control. and click "Save As Current Revision" when you find the version you want. apostrophePlugin also supports "areas. The usefulness of areas becomes clear when rich text slots are interleaved with media slots. Click "Save as current version" to permanently revert to that version. You can upload photos and select and embed videos freely without worrying about their size and format." preview versions by clicking on them. selecting from a list of slots approved for use in that area. Pick any version to preview it. labeled by date." You will be taken to the media area of the site. time and author and including a short summary of the change to help you remember what's different about that version. Editing users are able to add and remove slots from an area at any time." Areas are vertical columns containing more than one slot. Here you can select any of the images that have already been uploaded to the site. or upload new images. and because drag and drop is not actually much fun to use when a column spans multiple pages). === Editing Areas === In addition to single slots. === Editing Media === Click "Add Slot. 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.

This will take you to the reorganize tool. Note that you can select more than one category. a category selector will appear. You can also reorder the tabs at the top of any page by dragging and dropping. The page will now refresh and display a media browser that anyone on your site can use to see media in that category. This tool is only available to site administrators such as the `admin` user. It's possible to add a media page to the public-facing part of the site that displays only media in certain categories. add a new page and give it a title that relates to a media category. go to the home page. Once you select Media. Pick the appropriate category. a page where you can drag and drop pages to any position in the page tree for quick and painless reorganization of the site. Add new categories using the "Manage Categories" button at the left. and you can easily search YouTube from within the media interface. in the media browser area. select "Media" from the "Page Engine" menu. click "This Page. most recent first." then the gear icon. 36 | P a g e . there are times when you want to do something less linear. To do that. You can organize media with categories and tags. then click Save. like moving a page up or down in the page tree. click on the "Reorganize" button at the top of any page. with all of the browsing features that are standard in the media area. When the page settings dialog appears.You can do much the same thing with video. However. When the new page appears. YouTube is tightly integrated with Apostrophe. To do that. === 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 paste embed codes from other sites and add thumbnail images for those videos manually.

[ManualDesignersGuide Continue to the Designer's Guide] [ManualOverview Up to the Overview] 37 | P a g e .

. It is intended to be read by front end designers (also known as front end developers).php` files although full PHP programming skills are not required.yml`. === Title Prefix === By default.yml` to adjust settings. and you will be creating `.. title_prefix: en: 'Our Company : ' fr: 'French Prefix : ' # OR this way for a single-culture site title_prefix: 'Our Company' 38 | P a g e . You can do so by setting `app_a_title_prefix` in `app.= 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). Some familiarity with Symfony is expected. 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. This option supports optional internationalization: {{{ all: a: # You can do it this way. You will need to edit files like `apps/frontend/config/app.

php</tt> file. which can be assigned to individual pages. === How to Customize the Layout === By default. and in page template files. One strategy 39 | P a g e .yml` should be followed by a `symfony cc` command: {{{ .}}} Note that all changes to `app. the CMS will use the <tt>layout. If you wish.php</tt> file bundled with it./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. which decorates all pages.yml: {{{ all: a: use_bundled_layout: false }}} CMS pages will then use your application's default layout. you can turn this off via app.

How do you create your own template files? ''Don't'' alter the templates folder of the plugin.php</tt> to your application's template folder and customize it there after turning off use_bundled_layout. We provide these templates "out of the box:" * homeTemplate.is to copy our <tt>layout. These are standard Symfony template files with a special naming convention. === How to Customize the Page Templates === The layout is a good place for global elements that should appear on every page.php is used by our default home page. you should instead create your own a/templates folder within your application's modules folder: 40 | P a g e . Page template files live in the templates folder of the a module. But elements specific to certain types of pages are better kept in page templates.php homeTemplate. This does not delete the slots used by the previous template.php is the default template if no other template is chosen. so you can switch back without losing your work.php * defaultTemplate. and defaultTemplate. As always with Symfony modules. You can change the template used by a page by using the template dropdown in the breadcrumb trail.

that will be more forwards-compatible with new releases of the CMS. you'll need to adjust the `app_a_templates` setting in `app.php and defaultTemplate.yml` so that your new templates also appear in the dropdown menu: {{{ all: a: templates: home: Home Page default: Default Page mytemplate: My Template 41 | P a g e . or just start over from scratch.{{{ mkdir -p apps/frontend/modules/a/templates }}} Now you can copy homeTemplate.php if you don't like the way we present the login and logout options.php to this folder. If you add additional template files. if you can use CSS to match the behavior of our HTML to your needs. 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. You can also copy _login. In general.

{{{ <?php include_component('aNavigation'. 'active' => $page->slug. The navigation element. 'name' => 'normal')) ?> }}} Parameters * root (required) . 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. Accordion navigation is a compromise between showing the entire page tree and showing just the breadcrumb. 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. These links are rendered as nested `ul` elements. Accordion navigation includes everything that would be included in a breadcrumb trail. once used. 'accordion'. array('root' => '/'. Often the home page 42 | P a g e . plus the peers of all of those pages.php`. will no longer require any additional SQL queries no matter how many similar navigation elements are included in a template or layout. 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.This is the slug of the root page that the menu should start from. ==== Accordion Navigation ==== Accordion-style navigation can be easily included in your template by doing the following.}}} === Custom Navigation in Templates and Layouts === Apostrophe supports three types of navigation components.

'tabs'.php). Note that depending on your CSS you can render it as a vertical menu. see the "subnav" area of the `asandbox` project. 'active' => $page->slug. * active (required) .The unique name of the navigation element for your template.The page to build the navigation down to. array('root' => '/'. you'll need to use `url_for` to create an absolute URL to it: 43 | P a g e . 'tabs'.The unique name of the navigation element for your template * extras (optional) .* active (required) . will also recieve a css class of current * name (required) . {{{ <?php include_component('aNavigation'.This is the parent slug of the tabs that are displayed. array('root' => '/'.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'. see `a/templates/_subnav. 'extras' => array('/page/slug/or/absolute/url' => 'Label'))) ?> }}} If you wish to link to a Symfony action on the site as an additional tab.This is the page that if in the navigation will recieve a current css class of current * name (required) . 'active' => $page->slug. For an example. 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. 'name' => 'tabs')) ?> }}} Parameters * root (required) . 'name' => 'tabs'.

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 . 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. true) => 'Label') }}} ==== Breadcrumb ==== {{{ <?php include_component('aNavigation'.This is the last element that the navigation should descend to. Usually the home page * active (required) .the unique name of the navigation element for your template. Usually the current page * separator (optional) .The separator to use between items. 'name' => 'bread')) ?> }}} Parameters * root (required) .This is the root slug of the page the breadcrumb should begin with. 'breadcrumb'. array('root' => '/'. defaults to " > " * name (required) .{{{ 'extras' => array(url_for('module/action?my=parameter'. 'active' => $page->slug. === Inserting Slots in Layouts and Templates === #slots Of course.

<?php use_helper('a') ?> }}} {{{ <?php # Anywhere you want a particular slot ?> <?php a_slot('body'. The slot name will never be seen by the user." 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. 'aRichText') ?> }}} Notice that two arguments are passed to the <tt>a_slot</tt> helper. The second argument is the type of the slot. which distinguishes it from other slots on the same page. and an editor-configurable link) * aVideo (video from !YouTube and other providers) * aPDF (PDF documents from the media repository) 45 | P a g e . *Slot names should contain only characters that are allowed in HTML ID and NAME attributes*. The first argument is the name of the slot. underscores and dashes in slot names. It is a useful label such as `body` or `sidebar` or `subtitle`. We recommend that you use only letters. digits. "Out of the box.

* 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. array('multiline' => true)) ?> }}} 46 | P a g e . email addresses are rendered as obfuscated `mailto:` links. which can include valid HTML and UTF-8 entities). URLs are automatically rendered as links. Note that the special slot name `title` is reserved for the title of the page and is always of the type `aText`. 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'. You do this by passing an array of options as a third argument to the helper. 'aText') ?> }}} The behavior of most slot types can be influenced by passing options to them from the template or layout. valid HTML text. 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'. like this: {{{ <?php a_slot('aboutus'. ==== aText Slots ==== aText slots contain only plaintext (actually. and newlines are rendered as line breaks.

italic and links).js` (at the project level.The `multiline` option specifies that a plaintext slot should permit multiple-line text input. The `tool` option is one of the most useful options for use with rich text slots: {{{ <?php a_slot('subtitle'. Here is an example: {{{ 47 | P a g e . array('tool' => 'basic')) ?> }}} Here we create a subtitle rich text slot on the page which is editable. ==== aRichText Slots ==== Here is a simple example of a rich text slot: {{{ <?php a_slot('ourproducts'. The HTML they enter is filtered for correctness and to ensure it does not damage your design. 'aRichText') ?> }}} The rich text slot allows users to edit content using a rich text editor. 'aRichText'. but only with the limited palette of options provided in FCK's `basic` toolbar (bold. Read on for more information about ways to adjust these filters. not in the plugin) and they will be found automatically. Add these to `web/js/fckextraconfig. Note that you can create your own custom toolbars for the FCK rich text editor.

You can pass an array of HTML element names (without angle brackets). The default list of allowed tags is: `h3.js`. blockquote. 'aRichText'. td. array('tool' => 'basic'. hr. 'allowed-tags' => '<b><i><strong><em><a>')) ?> Note that we list both the `b` tag and the `strong` tag here. div. li.js`. caption. (You do not need to. tbody.) Other notable options to `aRichText` slots include: * allowed-tags is a list of HTML elements to be permitted inside the rich text. h6. Just add and override things as needed. em. We do this because different browsers submit slightly different rich text. 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'. }}} For more complete examples of what can be included here. h5. duplicate this entire file in `fckextraconfig. table. i. strong. ul. thead. or a string like that accepted by the PHP `strip_tags` function. pre` 48 | P a g e . see `apostrophePlugin/web/js/fckeditor/fckconfig. br. h4. code. th. strike. a. p. and should not. By default Apostrophe filters out most HTML elements to prevent pasted content from Microsoft Word and the like from wrecking page layouts.'Italic'] ]. nl.FCKConfig. b. tr.ToolbarSets["Sidebar"] = [ ['Bold'. ol.

attributes and styles. removes all inappropriate HTML attributes. caption. you may find it more convenient to use `app. p. "img" => array("src") ). table. }}} * allowed-styles is a list of CSS style names to be permitted. b. td. ul. a. high-performance HTML filter. h5. The format is the same as that used above for allowed attributes. h4. div. blockquote. Unlike `strip_tags` Apostrophe's robust. name. code. em. at the cost that a truly persistent user might manage to wreck the page layout. br. h6. pre] allowed_attributes: a: [ href. li. thead. By default. again on a per-element basis. You can alter this to allow more creative table styling. `aHtml::simplify`. '''be sure to keep them''' unless you specifically want to disable things that are normally considered essential): {{{ all: aToolkit: allowed_tags: [h3.yml` for this purpose (default values are shown. ol. i. on a per-element basis. no styles are permitted in rich text editor content. nl. target ] img: [ src ] allowed_styles: ~ }}} 49 | P a g e . th. hr. "name". If you want to change the default settings for allowed tags.* allowed-attributes is a list of HTML attributes to be accepted. "target"). Here is the default list of allowed attributes: {{{ array( "a" => array("href". tbody. strike. strong. tr.

'aImage') ?> 50 | P a g e . Our default is very nice. When they click "Save." they are invited to paste an RSS feed URL. which is smarter than PHP's `strip_tags` alone. Check out aFeed::getCachedFeed if you're into that sort of thing. 'Y m d' is not actually our default. ==== aImage Slots ==== Image slots are used to insert single still images from Apostrophe's built-in media repository: {{{ <?php a_slot('landscape'. 'aFeed') ?> }}} When the user clicks "Edit. * `'dateFormat' => 'Y m d'` lets you specify your own date and time format (see the PHP date() function). We apply our usual aHtml::simplify() method. You can change this behavior with the following options." the five most recent posts in that feed appear. * `'markup' => '<strong><em><p><br><ul><li>'` changes the list of HTML elements we let through in markup. The defaults are shown as examples: * `'links' => true` determines whether links to the original posts are provided. but very American.==== aFeed Slots ==== The aFeed slot allows editors to insert an RSS feed into the page: {{{ <?php a_slot('subtitle'. 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). We rock Symfony's caching classes to implement this. * `'interval' => 300` specifies how long we hold on to a feed before we fetch it again. so feel free to override it. This is important to avoid slowing down your site or getting banned by feed hosts. The title of each feed post links to the original article on the site of origin as specified by the feed. * `'posts' => 5` determines how many posts are shown.

* `'title' => false` specifies whether the title associated with the image in the media repository should be shown on the page. * `'resizeType' => 's'` determines the scaling and cropping style. The `c` style crops the largest part of the center of the image that matches the requested aspect ratio. The `s` style scales the image down if needed but never changes the aspect ratio. `constraints`. * `'flexHeight' => false` determines whether the height of the image is scaled along with the requested width to maintain the aspect ratio of the original. For instance: {{{ 'constraints' => array('aspect-width' => 4. Set this option to `true` to scale the height. * `'height' => 330` determines the height of the image. The media browser will show only media that match this aspect ratio. There is one more option. Note that in most cases you probably want to use an aButton slot for this sort of thing. Otherwise no image is displayed until an editor chooses one. 'aspect-height' => 3) 51 | P a g e . which takes an array of constraints that can be used to limit which images the user can select from the media repository. CSS can be used to display this title in a variety of interesting ways. The possible constraints are: * 'aspect-width' and 'aspect-height' specify an aspect ratio. * `'defaultImage' => false` allows you to specify the URL of an image to be displayed if no image has been selected by the editor yet. in pixels. Images are never rendered larger than actual size as upsampling always looks awful. so the image is surrounded with white bars if necessary (see `flexHeight` for a way to avoid this). These options suffice for most purposes. It is ignored if `flexHeight' => true is also present. in pixels.}}} Options are available to control many aspects of image slots: * `'width' => 440` determines the width of the image. * `'description' => false` specifies whether the rich text description associated with the image in the media repository should be shown on the page. Both must be specified. By default no constraints are applied. * `'link' => false` specifies a URL that the image should link to when clicked upon.

}}} * 'minimum-width' and 'minimum-height' specify minimum width and height in pixels. The original must be at least 400 pixels wide. 'constraints' => array('minimum-width' => 640. {{{ <?php a_slot('landscape'. and if you are using `flexHeight` you might not want to specify a height constraint. It is not mandatory to specify both. 'flexHeight' => true. 'aImage'. array('width' => 400. 'aImage'. Only media meeting these minimum criteria will be shown. 'aspect-width' => 4. The height will scale along with the width. 52 | P a g e . 'height' => 480. array('width' => 640. The aspect ratio must be 4x3. '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. * 'width' and 'height' specify that only images of that '''exact''' width and/or height should be selectable for this image slot. You are not required to use both options although it usually makes sense to specify both. Here are three examples: This slot will render the image 400 pixels wide. 'constraints' => array('minimum-width' => 400))) ?> }}} This slot will be render images at 640x480 pixels. {{{ <?php a_slot('landscape'. The selected image must be at least 400 pixels wide. The original must be at least 640 pixels wide.

Slideshows with images of varying dimensions can be confusing to look at unless the aspect ratio is the same. 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. 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. '''In addition to all of the options supported by the aImage slot''' (see above). 'aImage'. Fortunately slideshows support all of the sizing. Set this option to `true` for a random order. cropping and constraint options that are available for the aImage slot (see above). Left and right arrows to move back and forward in the slideshow appear above the image unless turned off by the `arrows` option. The exact behavior of the slideshow can be controlled by various options. Here is a simple example: {{{ <?php a_slot('travel'.{{{ <?php a_slot('landscape'. and the slideshow will advance when they click on an image. 'height' => 200. 'constraints' => array('minimum-width' => 400))) ?> }}} ==== aSlideshow Slots ==== Slideshow slots allow editors to select one or more images and display them as a slideshow. array('width' => 400. 53 | P a g e . 'aSlideshow') ?> }}} By default users can select any images they wish and add them to the slideshow. 'resizeType' => 'c'.

The default template is bundled with the slot and is named `_slideshowItem. However. * `'credit' => false` determines whether the credit field from the media repository is shown with each image. and the user can navigate manually by clicking to advance or using the arrows if present. Slideshows automatically repeat once they have advanced to the final image. In particular you might want to specify a default image. * `'interval' => false` sets the interval for automatic advance to the next image in the slideshow. By default the slideshow does not advance automatically. they are rendered as links to the button's destination URL. For automatic advance every ten seconds pass `'interval' => 10`. auto-advance stops. Here is an example: {{{ <?php a_slot('logo'. * `'itemTemplate' => slideshowItem` allows you to specify a different slideshow item template at the project level. You can turn this off by passing `false`. 54 | P a g e . they also allow the editor to specify a target URL and a plaintext title. If the title and/or description options are turned on.php`. 'aButton') ?> }}} Button slots support all of the options supported by the aImage slot (see above). they are taken to the URL. By default they do appear.* `'arrows' => true` indicates that arrows to move forward and backward in the slideshow should appear above the image. When they click on the button. If the user clicks on the slideshow. ==== aButton Slots ==== Button slots are similar to image slots.

so editors can search !YouTube directly from the repository in order to easily add videos. Viddler and Vimeo. 'aVideo') ?> }}} Note that you can disable support for embed codes from non-!YouTube services if you wish: {{{ all: aMedia: embed_codes: false }}} Note that the plugin itself ships with this feature disabled by default. ''' The video slot supports all of the options supported by the image slot''' (described above). Apostrophe wrangles the necessary embed codes and ensures that they render at the desired size. !YouTube is tightly integrated into Apostrophe. Apostrophe's media repository does not store video directly.yml` file of our sandbox project does turn it on. Here is an example: {{{ <?php a_slot('screencast'. but the `app. Either way. Embed codes for other video services can also be added to the media repository.==== aVideo Slots ==== The aVideo slot allows video to be embedded in a page. Instead Apostrophe manages videos hosted on external services such as !YouTube. with the following differences and exceptions: 55 | P a g e .

a clickable PDF icon is still displayed as a way of launching the PDF. ==== aRawHTML Slots: When You Must Have Raw HTML ==== 56 | P a g e . ==== aPDF Slots ==== The aPDF slot allows PDF documents to be embedded in a page. * The `height` option defaults to 240 pixels. If ghostscript and netpbm are installed on the server. We recommend using the `flexHeight` option if there is any doubt about the dimensions of the video. 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. As a general rule video hosting services do not permit cropping and choose their own approaches to letterboxing. The default image appears when no PDF has been selected yet. * 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. and clicking on them launches the PDF. aPDF slots support the following options. * The `constraints` option is supported.5x11 (use the aspect-width and aspect-height parameters). * The `constraints` option is available. The constraints will be applied based on the dimensions of the original video (if from !YouTube). * The `link` and `defaultImage` options are not supported. * The `defaultImage` option is available. * The `resizeType` option is ignored.* The `width` option defaults to 320 pixels. and can be used to select only PDFs with a certain aspect ratio. which behave as they do for the aImage slot (described above): * The `width` option defaults to 170 pixels. thumbnail previews of the first page are automatically displayed in PDF slots. such as 8. 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 case. If they are not available.

Visit the page with the following appended to the URL: {{{ ?safemode=1 }}} When this parameter is present in the URL. Editors tend to paste code that breaks page layouts on a fairly regular basis. And you can add more yourself. In these situations the aRawHTML slot is useful. Here is an example: {{{ <?php a_slot('mailinglist'. so you should think carefully before offering this feature to less experienced editors. That's it for the standard slots included with Apostrophe! Of course there will be more. This code is not validated in any way. 'aRawHTML') ?> }}} This slot displays a multiline text entry form where the editor can paste in HTML code directly. See the developer's section of this document. we're not big fans of raw HTML slots. 57 | P a g e . However there are times when you must have the embed code for a mailing list signup form service or similar third-party website feature. You can then click "Edit" and make corrections as needed. That's why our rich text slots filter it so carefully. There is a way to recover if you paste bad HTML that breaks the page layout.Honestly. safely defanged. raw HTML slots will escape their contents so that you see the source code.

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.=== 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.yml`: {{{ slot_variants: aSlideshow: normal: label: Normal options: interval: 0 title: false arrows: true compact: label: Compact options: interval: 0 title: true arrows: true itemTemplate: slideshowItemCompact autoplay: 58 | P a g e . So we've provided a better solution: slot variants. If you define variants like this in `app.

When the user switches between variants. the slot is refreshed with the new options in effect. Beginning in version 1.compact . you can address this by passing the `allowed_variants` option when inserting an area or slot: 59 | P a g e .label: Auto Play options: interval: 4 title: true arrows: false itemTemplate: slideshowItemCompact }}} And you have allowed these variants. provided that we have actually allowed those variants. Sometimes you will want to define variants on a slot that are only suitable for use in a particular place. How does this work? Each variant has a label and an optional set of options. you wouldn't want to allow an "extra-wide" image slot in a sidebar.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. 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: {{{ allowed_slot_variants: aSlideshow: .04. For instance.normal .

What about newly added slots? If there are variants for a slot.1/svn commit 1569 the `app_a_allowed_slot_variants` option did not exist'''. or there is a global `app_a_allowed_slot_variants` option. then the first allowed variant is used. But when you want to mix paragraphs of text with elements inserted by custom slots. That is. '''Prior to version 1.04 slot variant options must fully contradict each other''' for consistent results. variants also set a CSS class''' on the slot's outermost container. the CSS class `compact` will be set on that slot's outermost container. 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 you set the `interval` option for one variant.<?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.4. it is necessary to create a separate 60 | P a g e . 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. when the `compact` option is in use.04. 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. This is very useful for styling purposes. Specifically. since there are no other choices. 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 requirement is removed in version 1. '''In addition to changing options. If you allow only one variant. the options menu does not appear. === Inserting Areas: Unlimited Slots in a Vertical Column === #areas Slots are great on their own. If an `allowed_variants` option is present for a particular slot or area. you must set a value for it for every variant (even if that value is false).

This is tedious and requires the involvement of an HTML-savvy person on a regular basis. By default new slots appear at the top of an area. You insert an area by calling a_area($name) rather than a_slot($name): {{{ <?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." An area is a continuous vertical column containing multiple slots which can be managed on the fly without the need for template changes. You can now edit that first slot and save it. Fortunately apostrophePlugin also offers "areas. If you don't like this. you can change it for your entire site via `app.yml`: {{{ all: 61 | P a g e . Add more slots and you'll find that you are also able to delete them and reorder them at will.template file for every page. Click "Insert Slot" to add the first one.

array("allowed_types" => array("aText". In addition. you may find it is inappropriate to use certain slot types in certain areas. much as you would when inserting a single slot: {{{ a_area("sidebar". The `allowed_types` option allows us to specify a list of slot types that are allowed in this particular area. you can pass options to the slots of each type. deleting. array("allowed_types" => array("aText". "myCustomType"))) ?> }}} Notice that the second argument to `a_area` is an associative array of options. and reordering slots are themselves actions that can be undone through version control. You can specify a list of allowed slot types like this: {{{ <?php a_area("sidebar". In a project with many custom slot types.a: new_slots_top: false }}} An area has just one version control button for the entire area. This is because creating. 62 | P a g e . "myCustomType").

you want the content of a slot to be specific to a page. However. an editable page footer or page subtitle might be consistent throughout the site. if the content was the same on every page. it is sometimes useful to have editable content that appears on more than one page."type_options" => array( "aText" => array("multiline" => 1)))). Just set the `global` option to `true` when inserting the slot: {{{ <?php a_slot('footer'. array('toolbar' => 'basic'. After all. you wouldn't need more than one page. For instance. 'aRichText'. or at least throughout a portion of the site. 'global' => true)) ?> }}} The content of the resulting slot is shared by all pages that include 63 | P a g e . The quickest way to do this is by adding a "global slot" to your page template or layout. === Global Slots and Virtual Pages === Most of the time. }}} Here the `multiline` option specifies that all `aText` slots in the area should have the `multiline` option set.

Otherwise users with control only over a subpage could edit a footer displayed on all pages. For headers and footers. 'aImage'). Note that you can use the `global` flag with areas as well as slots: {{{ <?php a_area('footer'. Since there is no leading `/`. Conceptually. this page can never be navigated to. If you're not a PHP developer looking to use slots and areas to manage dynamic content related to your own code. array( 'allowed_types' => array('aRichText'. But if you have a larger amount of shared content. See below for more information about how to override this rule where appropriate. it's probably safe to skip this section. 'global' => true )) ?> }}} By default. Fortunately there's a solution: group your content into separate virtual pages.it with the `global` option. global slots can be edited only by users with editing privileges throughout the site. all global slots reside together on a virtual page with the slug `global`. the model begins to break down. 64 | P a g e . ==== Virtual Pages: When Global Slots Are Not Enough ==== Global slots will do the job for most situations that front end developers will encounter. this works very well. The `global` virtual page resides outside of the site's organizational tree and is used only as a storehouse of shared content. although it is sometimes useful to apply these techniques if you would otherwise have hundreds of global slots in your design.

so you should do this only if the user ought to have the privilege according to your own judgment. where `$id` might identify a particular user in the `sfGuardUser` table. As a rule of thumb. not area or slot names. Apostrophe will automatically create the needed virtual page object in the database the first time the slot is used. In other words. This technique can be used for areas as well. the CMS would be forced to load all of your biographies on just about every page of the site. only sitewide admins can edit slots included from other virtual pages. you might do something like this: 65 | P a g e .When you include a slot this way: {{{ <?php a_slot('biography'. But why is this better than using `'global' => true`? Two reasons: performance and access control. if you chose dynamically generated names for your slots and areas. you could manage dynamic content like biographies with just the global flag. ==== Access Control For Global Slots and Virtual Pages ==== By default. You can address this problem by passing `'edit' => true` as an option to the slot or area. add database IDs to virtual page slugs. users should be able to write their own autobiographies. but doesn't work well for biography slots. For that reason it's important to use a separate virtual page for each individual's biography. That's clearly not acceptable. ==== Performance. Global Slots and Virtual Pages ==== Yes. 'slug' => "bio-$id")) ?> }}} You are fetching that slot from a separate virtual page with the slug `bio-$id`. 'aRichText'. This overrides the normal slot editing privilege checks. For instance. But since Apostrophe loads all current global slots into memory the first time a global slot is requested on a page. etc. array('toolbar' => 'basic'. This is fine for headers and footers seen throughout the site.

Also keep in mind that normal pages can be moved around on the site.1 ==== Including Slots From Other "Normal" Pages ==== It's possible to use the `slug` option to include a slot from another normal. particularly with regard to the editing interface. 'aRichText'. === CSS: Styling Apostrophe === By default. You can avoid this by determining the slug option dynamically. keep in mind that you don't want to create a situation where the same slot is included twice on the page itself. which will break templates that contain explicit slugs pointing at their old locations. navigable page on the site. `apostrophePlugin` provides two stylesheets which are automatically added to your pages. or by using a virtual page instead (no leading / on the slug). 'slug' => "bio-$id". However. and we recommend that you keep these and override them as needed in a separate stylesheet of your own. Also note that setting 'edit' => false will also disable editing of the slot or areas for admin users as well.yml`: {{{ all: 66 | P a g e . you can turn them off if you wish in `app. There's a lot happening there. array('toolbar' => 'basic'. this is the behavior in the trunk and will be present in 1. However. 'edit' => $id === $myid)) ?> }}} Once again. if you do so. you can do this with areas as well.{{{ <?php $myid = sfContext::getInstance()->getUser()->getGuardUser()->id ?> <?php a_slot('biography'.

and we recommend you make your customizations there. This is automatically done for the stylesheet `web/css/main.a-area-body 67 | P a g e . you'll want to specify `position: last` for the stylesheets that should override them. Not IDs ==== You may want to style individual areas and slots without introducing wrapper divs to your templates. To do that.a: use_bundled_stylesheet: false }}} If you do keep our stylesheets and further override them. 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. Instead. ==== The Golden Rule: Use Classes.'' The id attribute contains the page ID.css` in our sandbox project. which differs from page to page and should never be used in CSS. ''Don't do that. pay attention to the CSS classes we output on the outermost wrappers of each area. take advantage of the classes on this div: {{{ .

The use of CSS floating makes it straightforward to lay slots and areas out as you see fit. you should use a separate template for each type of page. 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. ==== Why So Many Wrappers? ==== The wrapper divs output by Apostrophe are necessary to implement inline editing. 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. === CSS: Apostrophe UI === Apostrophe UI colors can be easily changed by overriding a small set of CSS styles bundled with the plugin. Located at the bottom of a. ==== Styling Slot Variants ==== Slot variants (choices on the "Options" menu for a given slot) also set CSS classes. This is very useful for styling purposes. These classes have the same name as the option. especially if you wish to display the rich text description of a media image to the right or left of the image itself. See the provided `a/templates/defaultTemplate.php` for an example of how to float one to the left of another.{ 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. See "Slot Variants: More Mileage From Your Templates" for details.css you will find this css: 68 | P a g e .

} .alt.0).a-admin .a-submit.a-variant-options { /* Border Color */ border-color: rgb(255. background-color: rgba(255.Default -------------------------------------*/ .150.a-admin-content a:link.a-admin .a-btn. .a-controls .a-cancel. .a-submit. } .alt.a-btn. aUI & Admin Colors .0).150.a-cancel.150. .{{{ /* 34. #a-global-toolbar #a-logged-in-as span. #a-global-toolbar #the-apostrophe { /* Apostrophe */ background-color: rgb(255.75). .alt. .a-admin-content a:visited. ul.0.a-history-browser. #a-personal-settings-heading span { /* Text Color */ 69 | P a g e .0. .

css into your site's CSS file and change the values to colors that work for your design.. `aUI('. it will create a simple button out of an any element. * examples * ''<A href="" class="a-btn">. When used alone.color: rgb(255.js` file.a-area-body').. We use this for updating buttons that are returned via AJAX. define the function `aOverrides()` in a newly created a project level `site.150.` will only affect buttons that are children of this CSS selector. ==== aUI() ==== There is a javascript function `aUI(). The `aUI()` call can be scoped using a CSS selector or a jQuery object by simply passing it into the function call. By default it touches every button the page and applies functional and aesthetic changes to the buttons." class="a-btn"/>'' 70 | P a g e . === CSS: Apostrophe Buttons === #abtn '''...a-btn''' * The base class for all buttons in the Apostrophe UI.` that decorates the buttons in a cross-browser compatible way on Dom Ready. It lives in `aUI. missing out on the Dom Ready call. ==== aOverrides() ==== This function is hooked into `aUI()` To use it. } }}} You can simply copy this section from a. This is helpful when working with technologies such as `Cufon` that need to be run on Dom Ready.0).</A>'' * ''<INPUT type="SUBMIT" value=".js`. If it exists. `aUI()` will call it whenever `aUI()` is executed.text.text.

a-icon-name''' * A second class is necessary for the ...no-label''' * Hides the text label of the button * '''...text..icon''' * Alters the button to make space for a sprite on the left side * '''..icon class to make sense..'' * '''.</A>'' * '''.text. * some site designs work better with white icons instead of black and using the .css file * example * CSS * ''.a-edit { background-image: url(/apostrophePlugin/images/a-icon-edit. ''Note: The ..nobg''' * removes background and border * example * ''<A href="" class="a-btn nobg">. A sprite is defined by the icon name class in the a.mini''' * Small button style with 10px type size * '''.* ''<LI class="a-btn">.alt''' * chooses the alternate CSS sprite and button color scheme for the button.alt class can be applied to the <BODY> to globally change all buttons across the site to use the alternate sprite.text.big''' * Large button style with 18px type size * example 71 | P a g e .</A>'' * '''.</LI>'' * '''.png).alt class in some situations helps improve the buttons legibility on a case-by-case basis. }'' * HTML * ''<A href="" class="a-btn icon a-edit">.

text.... use the following setting in `app.* ''<A href="" class="a-btn big">..</A>'' === Access Control: Who Can Edit What? === By default. But Apostrophe can also handle more complex security needs.flag''' * Hides the text label and displays it upon :hover * '''. === Requiring Login to Access All Pages === To require that the user log in before they view any page in the CMS. This is often sufficient for simple sites.text.flag-left''' * Hides the text label and displays it as a tooltip to the left of the button * '''. * Any authenticated (logged-in) user can edit any page. and add and delete pages.flag-right''' * Hides the text label and displays it as a tooltip to the right of the button * example * ''<A href="" class="a-btn flag flag-right">.</A>'' * '''. an unconfigured Apostrophe site that was not copied from our sandbox follows these security rules: * Anyone can view any page without being authenticated.yml`: 72 | P a g e .

In such situations you can set up different credentials to access the pages. so keep in mind that the default set of permissions. === Requiring Special Credentials to Edit Pages === Editing rights can be controlled in several ways. groups and `app. locked pages are only accessible to logged-in users.{{{ all: a: view_login_required: true }}} === Requiring Login to Access Some Pages === To require the user to log in before accessing a particular page. Of course. use the following app. while managing privileges allow them to also create and delete pages. To require the `view_locked` credential to view locked pages. 73 | P a g e . and you'll be able to distinguish user-created accounts from invited guests. By default.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. especially when users are allowed to create their own accounts without further approval. Editing privileges allow users to edit a page. It may seem a bit confusing.yml setting: {{{ all: a: view_locked_sufficient_credentials: view_locked }}} Then grant the `view_locked` permission to the appropriate sfGuard groups. on some sites this is too permissive. just navigate to that page as a user with editing privileges and click on the "lock" icon.

For instance. 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. You will need to do that if you are not using our sandbox project as a starting point. if you add such users to the `executive_editors` sfGuardGroup and grant that group the `edit` permission.'' This is useful for small sites. any user with `manage_sufficient_credentials` can always add or delete pages anywhere on the site. Note that the sfGuard "superadmin" user always has all credentials. then any logged-in user can edit anywhere. 2) Any user with `edit_sufficient_credentials` can always edit pages (but not necessarily add or delete them) anywhere on the site. regardless of all other settings.Read on if you believe you may need to override this configuration. Similarly. So 74 | P a g e . in addition to editing content. then you can give them full editing privileges with these settings: {{{ all: a: edit_sufficient_credentials: edit }}} ''If you do not specify any editing credentials at all. 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).

complete settings might be: {{{ all: a: 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. all logged-in users are potential editors. you can also grant these privileges directly via a group name: {{{ all: a: 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.'' For convenience. {{{ all: 75 | P a g e . If `app_a_edit_group` is not set.

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. 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). or to any user if `app_a_edit_candidate_group` is not set.a: edit_candidate_group: editors }}} Similarly. 76 | P a g e . 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). So a common setup might be: {{{ all: a: 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.

they are given the option of assigning editors for that page. with the candidate group being indicated by your `app_a_manage_candidate_group` setting. By default all new a pages are in the "published" state." Pages that are unpublished are completely invisible to users who do not have at least the candidate credentials to be an editor. === Publishing Pages. If you need to approach the matter more conservatively. 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 Choice and By Default === Apostrophe offers a "published/unpublished" toggle under "manage page settings.When a user with the right to manage a page opens the page settings. you can easily change this with the following `app. The same principle applies to "managing" (adding and deleting) pages. a user without appropriate privileges gets a 404 not found error just as if the page did not exist. 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.yml` setting: {{{ all: a: default_on: false }}} 77 | P a g e .

=== Limiting the Number of Children of Any One Page === Similarly. This is often a good choice. This makes sense for a site built by a few skilled people. This helps to avoid unwieldy side navigation and impose a bit more structure. which only permits tabs and grandchildren. which only permits tabs. editors can create pages nested as deeply as they wish. grandchildren (children of tabs). and great-grandchildren. but when there are many cooks in the kitchen you may want to impose some discipline. Set the `max_children_per_page` option to do this: {{{ all: a: # No page. For a very simple site with no breadcrumb trail in the layout. you might want to use `max_page_levels: 1`. grandchildren.=== Limiting The Depth of the Page Tree === By default.yml` by setting the `app_a_max_page_levels` option: {{{ all: a: # Allows tabs. You can do that in `app. and great-grandchildren max_page_levels: 3 }}} A setting of `3` allows tabs. including the home page. or `max_page_levels: 2`. you can limit the number of child pages that any given page can have. may have more than 8 direct children max_children_per_page: 8 }}} 78 | P a g e .

but for performance reasons you might be happier deferring this to a cron job that runs every few minutes. If you want to take this approach. On a development workstation.20. In a production environment you might specify `--env=prod`. There is a separate index for each environment.Note that only the immediate children of a page are counted against this limit. specify `--env=env`. 79 | P a g e .40.50 '' '' '' '' /path/to/your/project/symfony apostrophe:update-search-index -env=env }}} Note the `--env` option.30.yml: {{{ all: a: defer_search_updates: true }}} This speeds up editing a bit. This makes installation simple.10. === Deferring Search Engine Updates === By default pages are reindexed for search purposes at the time edits are made. set up a cron job like this: {{{ 0. But if you don't like cron. you don't have to enable it. Then turn on the feature in app.

=== Conclusion === That's it for the front end designer oriented section of the manual. you'll need to run this task with the appropriate environment parameter for the host in question after syncing content to it. you will need to build a new search engine index for production. [ManualDevelopersGuide Continue to Developer's Guide] 80 | P a g e . 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. 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 can also change the word count of search summaries: {{{ all: a: search_summary_wordcount: 50 }}} === Rebuilding the Search Index === When you deploy from development to production.

[ManualOverview Up to Overview] 81 | P a g e .

actions. We strongly recommend that you complete the [http://www. You can also generate the scaffolding in an existing or new Symfony plugin: 82 | P a g e . and front end design issues. which covers Apostrophe installation.org/jobeet/1_4/Doctrine/en/ Symfony tutorial] before attempting to add new features to Apostrophe. === 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. model class and form class in the `frontend` application of the project. We'll assume familiarity with Symfony development in this section./symfony apostrophe:generate-slot-type --application=frontend --type=mynewtypename }}} This task generates all of the scaffolding for a new. components. which is devoted to extending Apostrophe in new ways with your own code. templates and Doctrine model classes.symfonyproject. The designer's guide in particular is required reading to make good use of the following section. You can speed this process enormously by using the `apostrophe:generate-slot-type` task: {{{ . The above example generates the necessary module. the end-user experience of managing an Apostrophe site.= Apostrophe Manual = [ManualOverview Up to Overview] == Developer's Guide == At this point you should already be familiar with the preceding material. working slot type.

you add a new slot type like this: {{{ all: a: slot_types: mynewtypename: "Nice Label For Add Slot Menu" }}} When inserting an area in a template or layout. which was generated with the `apostrophe:generate-slot-type` task and then edited to implement its own features.yml`. In `app. you need to know how slot types work./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. to understand how to customize the behavior of your new slot type. you specify the allowed slot types like this: {{{ a_area('body'. 83 | P a g e . array('allowed_types' => array('aRichText'.yml`. 'mynewtypename'))). So let's dig into the workings of the `aFeed` slot. }}} You can also use your new slot type in standalone slots with the `a_slot` helper. or add the slot at the project level. Of course. This way you won't have problems later when you update apostrophePlugin. and also include it in the `allowed_types` option for individual Apostrophe areas in which you want to allow editors to add it. '''Don't put your slot in apostrophePlugin itself'''. Make a plugin of your own.{{{ . '''Reminder:''' to activate your slot you must add it to the list of allowed slot types for your project in `app.

such as `aFeedSlot`. A slot type generated with the `apostrophe:generate-slot-type` task will already contain the necessary supporting code in the `editView` component. The edit view displays the same slot as the user will see it after clicking the "Edit" button. Part I: Edit and Normal Views ==== With a few notable exceptions like our aImage slot.class. in `modules/aFeedSlot/actions/components. 3) A model class. you can render the form differently if you wish. The normal view presents the slot as a user will see it when not editing. This contains actions. most slots have an "edit view" and a "normal view. such as `aFeedForm`.1) A module. both views are present in the HTML whenever the logged-in user has sufficient privileges to edit the slot. components and partials to edit and render the slot within a page.''' 2) A form class. In this case. don't forget to enable the plugin in your !ProjectConfiguration class and the module in your settings.) ==== Slot Modules. the `aFeedSlot` module.yml file. For better editing performance. which inherits from the `aSlot` model class via Doctrine's column aggregation inheritance feature." rendered by editView and normalView components in the slot's module.php`: {{{ public function executeEditView() { 84 | P a g e . A simple _editView partial just echoes the form class associated with the slot: <?php echo $form ?> Of course. (Yes. we set this up for you when you use the `apostrophe:generate-slot-type` task. '''If the module lives in a plugin.

via the web or via an sfFileCache object if it has already been fetched recently.. // Careful.. even if the slot is new (in which case the array will be empty). The `aSlot::getArrayValue` and `aSlot::setArrayValue` methods are conveniences that simplify this for you. although it is possible for you to implement custom database columns instead of using Doctrine column aggregation inheritance as explained below.// Must be at the start of both view components $this->setup(). } } }}} Notice that the form is initialized with two parameters: a unique identifier that distinguishes it from other forms in the page. $this->slot->getArrayValue()). Your normal view's implementation is up to you. using `serialize` and `unserialize` to store PHP data in any way they see fit. One thing your normal view must do is provide an edit button at the top of the partial. This is usually not worth the trouble and the database schema changes it causes. `aSlot::getArrayValue` always returns a valid array. and a value fetched from the slot. You can also approach that problem by referencing the id column of the `a_slot` table from a foreign key in a related table. non-editing appearance. You don't need to worry about any of that. Most slots store their data in the `value` column of the slot table. And `aSlot::setArrayValue` accepts an array and serializes it into the `value` column. Try the slot type generator task to see a very simple working example. However it can be worthwhile if you need foreign key relationships and must avoid extra queries. and the normal view partial renders it. or provide some other way to edit the slot's settings. rather than the other way around. The normal view takes advantage of the information stored in the slot to render it with its normal. 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. The task generates code like this in the `_normalView` partial: {{{ 85 | P a g e . The feed slot's normal view component fetches the feed.

'permid' => $permid. 'pageid' => $pageid. public function __construct($id. 86 | P a g e . fully initialized and complete with any validation errors from unsuccessful edits.<?php include_partial('a/simpleEditWithVariants'. $defaults) { $this->id = $id. '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. array('name' => $name. By default this form is automatically echoed by the edit view component. specifically slots that have an edit button and take advantage of the edit view component. Here's the `aFeedForm` class: {{{ class aFeedForm extends sfForm { // Ensures unique IDs throughout the page protected $id. ==== Slot Forms ==== Most slots. $this->setDefaults($defaults). will have a form associated with them. } public function configure() { $this->setWidgets(array('url' => new sfWidgetFormInputText(array('label' => 'RSS Feed URL')))). parent::__construct().

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. However. $this->widgetSchema->setFormFormatterName('aAdmin'). which requires that we set up our own fields in the form class. it's usually best to avoid custom columns and use `setArrayValue` and `getArrayValue` instead. // Ensures unique IDs throughout the page $this->widgetSchema->setNameFormat('slotform-' . Doctrine forms do not distinguish between column aggregation inheritance subclasses and will include all of the columns of all of the subclasses. 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. } } }}} Notice that this form class is '''not''' a Doctrine form class. Also. which must cope with certain limitations of FCK). ==== Slot Components. but you may use another or render the form one element at a time in the `_editView` partial. or reports a validation error and refuses to do so.$this->setValidators(array('url' => new sfValidatorUrl(array('required' => true. Yes. which produces nicely styled markup. Here's the edit action for aFeedSlot. 'max_length' => 1024)))). The form formatter in use here is the Apostrophe form formatter. Part II: Actions ==== The edit action of your slot's module saves new settings in the form. $this->id . '[%s]'). `aFeedSlot` does inherit from `aSlot` via Doctrine column aggregation inheritance. which is exactly as the `apostrophe:generate-slot-type` task generated it: {{{ public function executeEdit(sfRequest $request) { 87 | P a g e .

`$this->editRetry()` should be called instead. if any return $this->editRetry(). $this->form->bind($value). } } }}} This action begins by calling `$this->editSetup()`.yml). // including foreign key relationships (see schema. $value = $this->getRequestParameter('slotform-' . which takes care of determining what slot the action will be working with. or save a single text value // directly in 'value'. if ($this->form->isValid()) { // Serializes all of the values returned by the form into the 'value' column of the slot. binds the form. return $this->editSave(). If there is a validation error.$this->editSetup(). serialize() and unserialize() are very useful here and much // faster than extra columns $this->slot->setArrayValue($this->form->getValues()). validates it. You can use custom columns. $this->id). } else { // Makes $this->form available to the next iteration of the // edit view so that validation errors can be seen. The action then fetches the appropriate parameter. and if successful saves the form's data in the slot with `setArrayValue` and calls `$this>editSave()` to save and redisplay the slot. array()). // This is only one of many ways to save data in a slot. $this->form = new aFeedForm($this->id. 88 | P a g e .

==== Custom Validation ==== Sometimes `$this->form` isn't quite enough to meet your needs. Or you might not be using Symfony form classes at all. You might have more than one Symfony form in the slot (although you should look at `embedForm()` and `mergeForm()` first before you say that). if it exists in the action. access control and everything else associated with the slot and let you get on with your job. So we suggest that you use other names for your validation data fields..Depending on your needs you might not need to modify this action at all. 89 | P a g e . // Grab it in the component $this->error = $this->getValidationData('custom'). The methods called by this action take care of version control.. 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`. 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'. // .

"?" . ==== Adding Database Columns ==== 90 | P a g e . from your normalView partial or elsewhere. "slug" => $page->slug. // The slug of the page where the slot lives. // Optional: use this if you are redirecting // from another page and need the entire page to render "noajax" => 1)) }}} For a fully worked example.==== Additional Actions ==== Things get interesting when you need to edit your slot with additional actions. we recommend taking a look at the `aButtonSlot` module. possibly actions that go to different pages like the "Choose Image" buttons of our media slots. http_build_query( array( "slot" => $name. "permid" => $permid. // The actual page we were looking at when we began editing the slot "actual_slug" => aTools::getRealPage()->getSlug(). 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). `$this->editSave()` and `$this>editRetry()`. The tricky part is linking to these actions. // Could be a virtual page if this is a global slot etc. 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') . You can write your own actions that use `$this->editSetup()`.

yml`. the Doctrine forms are not # of much use here and they clutter the project options: symfony: form: false filter: false # columns: # # You can add columns here. If you do add columns. If you are using `getArrayValue` and `setArrayValue` or otherwise storing your data in the `value` column. # This is how we are able to retrieve slots of various types with a single query from 91 | P a g e .. even though all of them are stored in the `a_slot` table. Here's how `aFeedSlot` is configured: {{{ aFeedSlot: # Doctrine doesn't produce useful forms with column aggregation inheritance anyway. so use a unique prefix # for your company. you'll need to know how this looks in `config/doctrine/schema.. # their names must be unique across all slots in your project. if you do need to add custom columns.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. if you do not need foreign key relationships it is # often easier to store your data in the 'value' column via serialize(). you'll never have to worry about this directly. # and slots often use serialization into the value column. However. However.

see the excellent Doctrine documentation. 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. while the `keyValue` field must contain the name of the type. but for more information about them. '''YOU MUST PREFIX YOUR CUSTOM COLUMN NAMES WITH A UNIQUE PREFIX''' to avoid collisions with other slots. you have to set the member variable 92 | P a g e . The `extends` keyword specifies the class we are inheriting from. already in the `aSlot` class. You can add new relations as well. To take the user straight to the editView of your slot. 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.'' To add extra columns at the database level. when a user adds a new slot to an area the user must then click the edit button before making changes to the slot. ''Note that the keyValue setting does not include the word Slot. Doctrine does not do this automatically.# a single table inheritance: extends: aSlot type: column_aggregation keyField: type keyValue: 'aFeed' }}} Take a look at the `inheritance` section. You don't need to worry about the details. uncomment `columns:` and add new columns precisely as you would for any Doctrine model class.

the right file is `lib/model/doctrine/mySlot. Editors in general will have access to the media module. If your slot lives in a plugin. Removing and Reordering Buttons via `app. If your slot lives at the project level. ==== Adding. a bar appears at the top of each page offering links to appropriate administrative features. the right class to edit is `plugins/myPlugin/lib/model/doctrine/PluginmySlot. === 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. You can add links of your own.php`. By default. this is equivalent to: {{{ all: a: extra_admin_buttons: users: label: Users action: 'aUserAdmin/index' class: 'a-users' reorganize: label: Reorganize action: a/reorganize class: a-reorganize 93 | P a g e . or change the order of the buttons to be displayed.yml` ==== The simplest way to add new buttons is to set `app_a_extra_admin_buttons` in `app.`editDefault` to `true`. See the aRichTextSlot or aTextSlot for an example. Admins will see a button offering access to the sfGuardUser admin module. This allows you to add buttons that point to Symfony actions you coded yourself.yml`.class.php`.class.

blog }}} Global buttons will be displayed in the order specified here (specify button keys.users . The key is used in `app. The label is also looked for in the `apostrophe` i18n catalog. 94 | P a g e . even if they were added by plugins. If you do not specify `app_a_global_button_order` the buttons will be displayed in alphabetical order by name.media .reorganize . and the blog plugin. etc. and automatically internationalized by Symfony. or discard buttons that were added by plugins. not button labels).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`). the media repository. Note that the key and the `label` field are different. Any buttons you leave off the list will not be displayed at all.}}} If you override this setting you will almost certainly want to keep these two buttons in the list. will add `Blog` and `Events` buttons. use `app_a_global_button_order`: {{{ all: a: global_button_order: . To specify the order of the buttons. if installed. `label` is shown to the user.

yml`. The second argument is the label of the button. which may contain markup and will be automatically internationalized. typically).getGlobalButtons` event. You can do both of these things by responding to the `a. 'aMedia/index'. Or perhaps you want to add a button that targets a specific engine page. And the fourth is a CMS class to be added to the button. The third is the action (in your own code. See the apostrophePluginConfiguration class for the // registration of the event listener.yml` is the easiest way to add buttons. Perhaps you're writing a plugin that adds new features to Apostrophe and should register new buttons on its own. 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. 'Media'. } } }}} The first argument to the `aGlobalButton` constructor is the name of the button.==== Adding Buttons Programmatically ==== `app. but sometimes it's not enough. 95 | P a g e . 'a-media'))). This is used to refer to that button elsewhere in your code and in `app. static public function getGlobalButtons() { aTools::addGlobalButtons(array( new aGlobalButton('media'. see // the provided stylesheet for how to correctly specify an icon to go // with your button.

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').which is typically used to supply your own icon and a left offset for the image to reside in. 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. } } }}} For more information see "Engines: Grafting Symfony Modules Into the CMS Page Tree" below. in the initialize method of your plugin or project's configuration class. if ($user->hasCredential('media_admin') || $user->hasCredential('media_upload')) { aTools::addGlobalButtons(array( new aGlobalButton('media'. Now. like our media system. $mediaEnginePage))). If your own plugin. 'a-media'. 'Media'. implements its administrative page as an apostrophe CMS engine page under `/admin` and also might have public engine pages elsewhere on the site. 'aMedia/index'. you'll want to make sure your button targets the "official" version. 96 | P a g e . // Only if we have suitable credentials $user = sfContext::getInstance()->getUser().

'' To take advantage of this feature. This is a very powerful way to integrate non-CMS pages into your site. if you require the Symfony routing cache. 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. However. all of the virtual "pages" associated with the actions of the module move as well. The media browser of apostrophePlugin already takes advantage of it. and the forthcoming apostropheBlogPlugin will as well. When the engine page is moved within the site.3 and 1. you can still use engines as long as you don't install the same engine at two points in the same site. ''A single engine module can now be grafted into more than one location on a site. with all URLs beginning with that page slug remapped to the actions of the engine module. }}} The bar at the top of each page will now feature your additional button or buttons. you must disable the Symfony routing cache.getGlobalButtons'. === 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. Even without multiple instances. 97 | P a g e . Disabling the routing cache is the default in Symfony 1. It's important that the bar remain manageable and convenient for site admins. array('aMediaCMSSlotsTools'. 'getGlobalButtons')). Usually no more than one per plugin is advisable. ''Note:'' you should not add large numbers of buttons to the bar.$this->dispatcher->connect('a.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).

yml`. To create a a engine. beginning at a point somewhere within the CMS page tree. Then change the parent class from `sfActions` to `aEngineActions`. } } }}} Now. The following are sample routes for a module called `enginetest`: {{{ 98 | P a g e . implement your own `preExecute()` method in which you call Apostrophe's helper method for engine implementation. parent::preExecute(). this admin generator actions class has been modified to work as an Apostrophe engine: {{{ class departmentActions extends autoDepartmentActions { public function preExecute() { aEngineTools::preExecute($this).Engines should always be used when you find yourself wishing to create a tree of dynamic "pages" representing something other than normal CMS pages. be sure to call `parent::preExecute` from that method. ''If your actions class must have a different parent class''. For example. Otherwise it will not work as an engine. Make sure you give these routes the `aRoute` class in `routing. Feel free to test its functionality normally at this point. or a catch-all route for all of them. NOTE: if your actions class has a `preExecute` method of its own. create routes for all of the actions of your module. begin by creating an ordinary Symfony module.

# Engine rules must precede any catch-all rules enginetest_index: url: / param: { module: enginetest, action: index } class: aRoute enginetest_foo: url: /foo param: { module: enginetest, action: foo } class: aRoute enginetest_bar: url: /bar param: { module: enginetest, action: bar } class: aRoute enginetest_baz: 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 above:

{{{ enginetest_action: 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 aDoctrineRoute:

{{{ a_event_show: 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 `aDoctrineRouteCollection`:

{{{ department: class: aDoctrineRouteCollection options: model: module: prefix_path: column: Department department '' 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 case).

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:

{{{ /test1 }}}

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:

{{{ /foo }}}

To the appropriate rule.

101 | P a g e

If you simply wish to customize the behavior of just part of a page. you can create subnavigation between the actions of your module by writing normal `link_to` and `url_for` calls: {{{ echo link_to('Bar'. add the following to `app. as otherwise normal CMS pages are not permitted on your site. 102 | P a g e . Note that as a natural consequence of this design. Once you have established your routes. when the engine page is accessed with no additional components in the URL. Be sure to keep the "template-based" entry in place. a custom page template or custom slot will better suit your needs. engine pages cannot have subpages in the CMS. it is appropriate to use engines only when you wish to implement "virtual pages" below the level of the CMS page. `aRoute` will match it to the rule with the URL `/`. 'enginetest/bar') }}} To make the user interface aware of your engine.yml`: {{{ all: a: engines: '': 'Template-Based' enginetest: 'Engine Test' }}} Substitute the name of your module for `enginetest`.As a special case. In general.

Here's how to sort it out. 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. the a routing system will find the first engine page in the site that does match the route. But what if you need to generate a link to a specific engine action from an unrelated page? For instance. You can test to make sure the engine page exists like this: {{{ <?php if (aPageTable::getFirstEnginePage('enginetest')): ?> <?php echo link_to('Bar'. this will throw an exception and generate a 500 error. With multiple instances of the same module. and generate a link to that engine page. things are simple: links to routes for that engine always point to a URL beginning with that page. 103 | P a g e . '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.Linking to the "index" action of an engine page is as simple as linking to any other page on the site. 'enginetest/bar') }}} If the current page is not an engine page matching the route in question. Note: if there is currently no engine page for the given engine. 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'. things get trickier.

When the page for which the link is being generated (the current CMS page) is an engine page for 'blog'.4): {{{ <?php echo link_to('Jane's Blog'. If the current page is not an engine page for the engine in question. 'blog/index') ?> <?php aRouteTools::popTargetEnginePage('blog') ?> }}} 104 | P a g e . links generated on that page will point back to that page by default.There are three simple rules: 1. For many purposes. When there is only one engine page on the site for a particular engine module 'blog'. as explained above. If you do not specify this parameter. you get the first matching engine page. you add an extra `engine-slug` parameter to the Symfony URL (beginning in Apostrophe 1. even if other engine pages for that engine module do exist. If you have an `aPage` object and wish to target it just set `engine-slug` to `$myPage->slug`. 3. When you wish to target a specific engine page with link_to and url_for calls. the first matching engine page found in the database is used by default. 2. It is used only to determine which engine page to target. links always target that page by default. 'blog/index?engine-slug=/janesblog') ?> }}} The `engine-slug` parameter will automatically be removed from the URL and will not appear in the query string. 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'. this is all you need.

"But how can editors change settings for that particular engine page?" Well.yml`.4. 105 | P a g e . and link to it from your engine page. you may pass either a page slug (like `/janes-blog`) or an `aPage` object to the `aRouteTools::pushTargetEnginePage` method. 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. 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. ==== 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. as it adheres more closely to the philosophy of newer versions of Symfony which emphasize dependency injection and frown on global state. you must not enable the Symfony routing cache if you wish to include multiple engine pages for the same engine in your site. We recommend using `engine-slug` rather than pushing and popping engine pages wherever practical. This is how our media repository associates particular engine pages with particular media categories. you could create your own settings action of course. how do I distinguish them?" That part is easy. and you can always do normal Symfony development. Apostrophe is still Symfony. 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. If you have upgraded an older project you may need to manually shut it off in `apps/frontend/config/routing.For convenience. it is advisable to always `pop` after `push`ing a different engine page in order to avoid side effects. Since you may find yourself writing partials and components that are included in other pages. Again. Remember. The routing cache is turned off by default in both Symfony 1.3 and 1. URLs generated after the `pop` call revert to the usual behavior.

$this->widgetSchema->setHelp('media_categories_list'.Assuming your engine module is called `blog`. Create a form class called `blogEngineForm`. $this->widgetSchema->setLabel('media_categories_list'. If it exists.'(Defaults to All Cateogories)'). 2. Create a `blog/settings` partial that renders `$form`. 'model' => 'aMediaCategory'))). then add back the fields you're interested in. 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. 'model' => 'aMediaCategory'. new sfWidgetFormDoctrineChoice(array('multiple' => true. $this->setWidget('media_categories_list'. If both forms validate. This partial can be as simple as `<?php echo $form?>` if you wish. $this->setValidator('media_categories_list'. 'required' => false))). this is all you have to do: 1. Take a look at `aMediaEngineForm`: {{{ class aMediaEngineForm extends aPageForm { public function configure() { $this->useFields(). Apostrophe will automatically look for this form class. followed by your engine settings form). 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. Apostrophe will render both the standard page settings form ```and``` your engine settings form if that engine is selected. 'Media Categories'). 106 | P a g e . Apostrophe will save them consecutively (the page settings form. The constructor of this class must accept an `aPage` object as its only argument. Then you can extend the `aPageForm` class and immediately remove all fields. new sfValidatorDoctrineChoice(array('multiple' => true.

$this->widgetSchema->setFormFormatterName('aAdmin'). $this->widgetSchema->getFormFormatter()->setTranslationCatalogue('apostrophe'). } } }}} The corresponding schema is: {{{ aMediaCategory: tableName: a_media_category actAs: Timestampable: ~ Sluggable: ~ columns: id: type: integer(4) primary: true autoincrement: true name: type: string(255) unique: true description: type: string relations: 107 | P a g e .$this->widgetSchema->setNameFormat('enginesettings[%s]').

108 | P a g e . Select your engine and save your changes. but if you don't you'll need to make sure your `updateObject()` and `save()` methods do the right thing in your own way.MediaItems: 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 Pages: 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`. ==== 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. Note that engine pages can be moved about the site using the normal drag and drop interface. The page will refresh and display your engine. You don't have to extend `aPageForm` and use a relation in this way.

Thanks to Quentin Dugauthier for his assistance in debugging these features.yml setting. We plan to do this in the future. === Refreshing Slots === This is not necessary for any of our standard slot types. The search index also distinguishes between cultures. to the sf_default_culture settings. which updates all current slots by calling their `refreshSlot()` method: {{{ . === 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./symfony apostrophe:refresh --env=prod --application=frontend }}} Again. you might wish to take advantage of the `apostrophe:refresh` task.You can create your own subnavigation within your engine page. if your custom slot types contain metadata that should be refreshed nightly. you are editing the version of the content for your current culture. When you edit. as usual. 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. We suggest overriding appropriate portions of your page layout via Symfony slots. 109 | P a g e . The user interface for editors is not yet internationalized. In future we may implement a handshake with YouTube via this task to check whether video slots are still pointing to valid videos. 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. However. The user's culture defaults.

query-language.html You can use Lucene to index your own data and take advantage of that fully. You can use the `-allversions` option to specify that older versions of slots should be refreshed as well: {{{ . You can grab the search query string from $sf_data->getRaw('q').lucene.search. Keep in mind that the entire Zend library is available to you already since it's one of Apostrophe's requirements. This is Zend Lucene search. Following this approach you don't need to know much about Apostrophe's internals at all./symfony apostrophe:refresh --env=prod --application=frontend --allversions }}} === Extending Search === You can override the a/search action at the application level.com/manual/en/zend.For performance reasons this task only looks at the latest version of each slot. You can do that in three ways. and bring in more results for other types of data via a Symfony component of your own. 110 | P a g e . The easiest way is to override the template that displays the results (a/searchSuccess). Just like any other Symfony application. so Lucene syntax is allowed: http://framework.zend.

for instance. The third supported way is to interleave your own results with the page search results. Apostrophe's a/search action is designed to be extended in this way. and the page slugs look like this: {{{ @a_blog_search_redirect?id=50 }}} 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. based on their search ranking. This makes sense only if the results are somewhat reasonable to compare .The second supported approach is to store (or mirror) your additional data using Apostrophe virtual pages. all content is stored as Apostrophe virtual pages. if it begins with a `@` or contains a `/` internally (not at the beginning). as introduced in ManualDesignersGuide. In the blog plugin. Fortunately. which should extend BaseaActions. 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 . Virtual pages are included in Apostrophe search results if the virtual page slug appears to be a valid Symfony URL: that is. All you have to do is implement the searchAddResults method in your application-level aActions class. articles and web pages are reasonably similar and Zend will probably produce search rankings that mix reasonably well. apostropheBlogPlugin uses this technique to create valid links when a search matches a published blog post.

// This is at the application level. $q) { // $values is the set of results so far. otherwise it will not be sorted by score.php class aActions extends BaseaActions { protected function searchAddResults(&$values.php) to distinguish result types. 'class' => 'blog_post'. // Example: $values[] = array('title' => 'Hi there'. // // IF YOU CHANGE THE ARRAY you must return true. // 'link' => 'http://thissite/wherever'. apps/frontend/modules/a/actions/actions. // $q is the Zend query the user typed. // return true.8) // // 'class' is used to set a CSS class (see searchSuccess. // // Override me! Add more items to the $values array here (note that it was passed by reference). 'summary' => 'I like my blog'. passed by reference so you can append more. 'score' => 0.class. 112 | P a g e . // // Best when used with results from a aZendSearch::searchLuceneWithValues call.

which then get sorted by score with everything else and presented as part of the search results. That class provides methods you can call from your model classes to add search indexing to them. For a working example. }}} That method calls back to your doctrineSave method. and also expects your class to provide some methods of its own that get called back. If you are wondering how to integrate Zend Search into your own modules. which does it for our own data types.class. null. which is usually a simple wrapper around parent::save for a Doctrine model class.} } }}} This method's job is to add more results to the results array (note that it is passed by reference). The save() method calls: {{{ // Let the culture be the user's culture return aZendSearch::saveInDoctrineAndLucene($this. $conn). see the aMediaItem class. but you can extend it as needed: {{{ 113 | P a g e . check out our aZendSearch class (apostrophePlugin/lib/toolkit/aZendSearch.php).

which should invoke aZendSearch::updateLuceneIndex with an associative array of fields to be included in the search index: {{{ public function updateLuceneIndex() { aZendSearch::updateLuceneIndex($this. ". 'tags' => implode(". 'description' => $this->getDescription(). 'credit' => $this->getCredit(). $this->getTags()) )).public function doctrineSave($conn) { $result = parent::save($conn). 'title' => $this->getTitle(). } }}} And it also calls back to updateLuceneIndex. That's great here because with 114 | P a g e . but not stored in full for display purposes. } }}} The array we pass as the second argument to updateLuceneIndex contains fields that should be indexed so that we can search on them. return $result. array( 'type' => $this->getType().

'summary' => $this->getShortSummary(). Specifically. You can 115 | P a g e . 'view_is_secure' => false)). 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: {{{ aZendSearch::updateLuceneIndex($this. }}} Note the second argument. 'url' => 'mymodule/show?id=' . you'll need to store: * The title ('title') * The summary text ('summary') * The URL ('url').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. null. // Or perhaps $this->getCulture() depending on your needs array('title' => $this->title. array('text' => $this->getFullSearchText()). which is the culture for this object. $this->id. And that is crucial if you want to display complete search results with our unmodified a/search template.

pass null to use the current user's culture which often makes sense if they have just edited the object. actual body text) into a single string and return that. $conn). } }}} 116 | P a g e . Apostrophe search returns only results for your current culture. which is usually just a wrapper around parent::delete: {{{ public function doctrineDelete($conn) { return parent::delete($conn). tags. Your 'getFullSearchText' method would typically just append all of the fields that contain text relevant to searching (title. 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. } }}} That method will call back to your doctrineDelete method. null.

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

array( 'permid' => 1. 'add'. }}} You can explicitly specify that it should or should not be at the top of the area: 120 | P a g e . Often it's easiest to copy the previous version: {{{ $slot = $slot->copy(). specify 'add' rather than 'update'. 'slot' => $slot)). // Make your changes to $slot.$slot->value = $title. $page->newAreaVersion('title'. You do not have to specify a permid since that is generated for you when adding a new slot: {{{ $page->newAreaVersion('myareaname'. }}} Note that you want to use a new slot object. then call newAreaVersion }}} If you want to add an entirely new slot. array('slot' => $slot)). $slot->save(). 'update'.

}}} The default is to add the slot at the top. ==== Looping Over All Slots in a Page ==== Looping over slots is dangerous. array('slot' => $slot. because there can be slots that are not actually used in the current template if a page has changed templates. $permid = $areaVersionSlot->permid. 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. 'top' => false)). // Now you can do things with $slot } } 121 | P a g e . 'add'. depending on your goals.{{{ $page->newAreaVersion('myareaname'.

All you have to do is extend the aEmbedService class and implement the methods you find there. In addition. The APIs of these methods have deliberately been kept very simple. description and tags manually * Automatically retrieve a thumbnail when adding the item Beginning in Apostrophe 1. === Adding Support For New Embedded Media Services === Out of the box. 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). version 1. You need the permid to save a new revision of the slot.5 (and currently available in the svn trunk). you can add additional services at the project level or in a Symfony plugin.}}} 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. fetching the appropriate information from the API for the service you're interested in.5 of Apostrophe supports embedding almost any media service (such as Youtube) by pasting embed tags via the "Embed Media" button. All versions of the same slot will have the same permid. Documentation of the expected return values is provided in comments in the aEmbedService class. 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. 122 | P a g e . Apostrophe has native support for Youtube and Vimeo.

we recommend copying the aYoutube or aVimeo class as a starting point. (In Apostrophe 1.5 if you are interested in adding support for custom services.apostrophenow.class: aYoutube media_type: video . we love bug reports..yml`: {{{ all: aMedia: embed_services: . you'll need to tell Apostrophe about it with appropriate settings in `app. You can choose to remove them if you wish by not including them in your settings. Visit the [http://trac. After you write your class.) === Conclusion === Thanks for checking out Apostrophe! We hope you're excited to experiment with our CMS. We recommend moving to the trunk and soon to version 1. as it makes it easier to ensure you are returning data in the right format.class: aMyservice media_type: video }}} Here the standard YouTube and Vimeo services have been kept in place.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.Although you can start from scratch. there's a good chance you'll build custom slots and engines.class: aVimeo media_type: video . and even send us bug reports.org/ Apostrophe Trac] to 123 | P a g e .. If you've read this far. Hey.

[wiki:ManualI18N Continue to Internationalizing Apostrophe] [ManualOverview Up to Overview] 124 | P a g e .google. And also be sure to join the [http://groups.com/group/apostrophenow apostrophenow Google group] to share your experiences with other Apostrophe developers. requests and concerns.submit your reports.

However. 125 | P a g e ./symfony doctrine:build --all --and-load }}} Again. creating the database tables for the blog plugin is a natural part of the usual command: {{{ . which includes the blog plugin. or version 1. the blog plugin can be added to an existing Apostrophe Symfony project. which includes the blog plugin. '''the quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the 1. of course. overwriting all of your existing data.4 stable branch] (or the trunk) of the sandbox project'''. enable it correctly. The 1. in which case you can just install that according to [wiki:ManualInstallation#CheckItOutFromSubversion the directions]. == Installation == In a new project.4 stable.0 stable release of apostrophePlugin is missing necessary supporting features for apostropheBlogPlugin. The '''quickest route is to [wiki:ManualInstallation#CheckItOutFromSubversion check out the the sandbox project]'''. To do so you'll need to install the plugin via `plugin:install` or svn externals (we recommend the latter). 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.= Apostrophe Manual = [ManualOverview Up to the Overview] == Requirements == Your project must contain the svn trunk version of apostrophePlugin. and add the relevant tables to your database without.

To locate the dashboards./symfony cc . If you are not using MySQL./symfony doctrine:build --all-classes . Enable the specific plugins you want. Make sure you have built your model. you can add them after you have performed the above steps by visiting the permissions and groups dashboards while logged in as the superuser. with the blog plugin listed after the main Apostrophe plugin.class." and then click on "Permissions Dashboard" and "Groups Dashboard" at the left./symfony apostrophe:migrate --env=dev }}} This task requires MySQL.sql and review that file for tables beginning with `a_blog`. use doctrine:build-sql to generate SQL commands in data/sql/schema. === Creating Your Engine Pages === 126 | P a g e . Otherwise certain class files will not be found. For security reasons we do NOT automatically add these via the `apostrophe:migrate` task. However.'''First make sure that apostropheBlogPlugin is enabled AFTER apostrophePlugin in your config/ProjectConfiguration. taking care to specify the correct environment: {{{ . Be sure to create `blog_admin` and `blog_author` permissions and add them to your `admin` and `editor` groups. log in as the superuser. BACK UP YOUR DATABASE. Once you have enabled the plugin. you need to add the appropriate database tables.php file.''' Do NOT use `enableAllPluginsExcept` for this purpose. and then run the `apostrophe:migrate` task. === 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. form and filter classes and cleared your cache. Fortunately the `apostrophe:migrate` task is automatically extended to support this when the blog plugin is present. or the corresponding entries in `sf_guard_user_group_permission`. click on "Users.

Navigate to the home page 2.You're almost there. but you still need to create "engine pages" where your blog posts and events will be displayed. click the "Blog" icon at the top of any page. However it offers substantial benefits." 127 | P a g e . 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. Log in as admin 3. click "This Page. Click Save. 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. To create your front end engine pages for blog posts and upcoming events: 1. 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. Although there is a separate back end for editing and managing blog posts. Then click "New Post." then click the "Gear" icon 5. The distinction between the back end blog administration page and the front end blog engine page can be a bit confusing. Click "This Page. accessed via the gear. Now repeat these steps for the "Events" engine. == Posting to the Blog == To post to the blog. That's why we use separate pages for this purpose." then "Add New Page." giving it the name "News" or "Blog" depending on your preference 4. When the new page appears. Set "Page Engine" to "Blog" 6. creating and naming the front end pages is up to you.

or insert multiple recent blog posts with the "Blog Posts" slot. and also allows you to select specific categories. exactly as you would if you were creating a regular Apostrophe page. found in the post. Since all blog posts must have unique titles this is a very effective way to pick a 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. 128 | P a g e . == Blog Post Slots == apostropheBlogPlugin provides two good ways to insert blog posts into your regular pages. You can also create additional blog engine pages which are locked to a single category. the images are presented as a click-to-advance slideshow. === Blog Post Excerpt Options === By default.Give your post a title. first create the page. The "Blog Post" slot invites you to search by title. If it is greater than one. then start adding slots. The use of categories allows you to present relevant content on any page without foreknowledge of what that content will be. To lock a blog engine page to a single category. The "Blog Posts" slot asks how many of the most recent blog posts you wish to display. The body of your blog post is a full-fledged Apostrophe "area." so it can contain rich text. == 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. blog post slots display a short excerpt from the post along with the first image. if any. then switch it to the blog engine. slideshows. video and other Apostrophe features. You can insert a single blog post with the "Blog Post" slot. and then select one or more blog post categories from the category selector that appears.

For instance.yml`.com . These options are often enough. You can add additional templates to your project by overriding the `aBlog` settings in `app.yml` (of course you should merge this under your existing `all` heading): {{{ all: aBlog: templates: singleColumnTemplate: name: Single Column areas: ['blog-body'] twoColumnTemplate: name: Two Column areas: ['blog-body'. Note that you must begin by copying this entire section into `app. 'blog-sidebar'] comments: false # add_this: punkave aEvent: 129 | P a g e # Username for AddThis -. * `excerptLength` determines the maximum word count of the excerpt. Read on for more possibilities. == Customizing Blog Post Templates == All blog posts have a blog post template that determines their basic structure. including displaying the entire blog post as part of a regular CMS page. The two-column posts have a narrow second column which is great for associated images and videos. "out of the box" the blog plugin allows for one-column and two-column blog posts.http://addthis. using exactly the same options available to slideshow slots (see ManualDesignersGuide). But you can do more.* `slideshowOptions` can be used to override the behavior of the slideshow.

and even change the Apostrophe areas that make up the blog post. You can customize the appearance of these templates when the blog post is seen in on the blog engine page. Please do not remove templates that are already in use for posts on your site. so we recommend specifying a list of area names for each template.yml` excerpt). first add your new templates and change the template setting for each existing post. And you can customize the appearance of blog posts when inserted as slots by overriding `aBlog/singleColumnTemplate_slot` and `aBlog/twoColumnTemplate_slot`. When you insert a blog post slot in a page template like this: {{{ <?php a_slot('blogpost'. or replace them entirely. if you are inserting blog posts on pages with different page templates and different amounts of space available. by overriding the `aBlog/singleColumnTemplate` and `aBlog/twoColumnTemplate` partials. If you need to do that. you'll want the `subtemplate` option. The templates you specify will be the choices on the "Template" dropdown when creating a blog post.templates: singleColumnTemplate: name: Single Column areas: ['blog-body'] }}} Once you have copied these settings to your own `app. Often these options are enough.yml`. which returns just the text of an entire blog post. or you wish to use teasers in some cases and full-fledged blog posts in others. 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. you can add additional templates in addition to the one-column and two-column templates. However. This is used to implement the `getText` convenience method. array('subtemplate' => 'inMyTemplate')) ?> 130 | P a g e . This is not currently used in the core of the blog plugin but does come in handy in application-level code at times. And you can override the templates for the RSS feed by overriding `aBlog/singleColumnTemplate_rss` and `aBlog/twoColumnTemplate_rss`. 'aBlogSingle'.

But it may work well with your own overrides of those templates.}}} The blog plugin will append `_inMyTemplate` (note the underscore) to the blog post's template name. 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. array('full' => true)) ?> }}} In this case. if the blog post is a single column post. `_twoColumnTemplate_inMyTemplate. `_singleColumnTemplate. `_singleColumnTemplate_inMyTemplate.php` will be used. 'aBlogSingle'.php` will be used. That is. You can simply force the blog slot to use the same template that the blog engine page would use: {{{ <?php a_slot('blogpost'. and if the blog post is a two column post. array('template_options' => array('singleColumnTemplate' => array('subtemplate' => 'inMyTemplate')))) ?> }}} There are two more ways to change the template used for a blog post. You can also specify subtemplates for specific blog post templates: {{{ <?php a_slot('blogpost'. instead of the usual `_slot`.php` is used. exactly as it would be on the engine page. 'aBlogSingle'. The last option is to simply force the use of a specific blog post template: 131 | P a g e .

are typically presented in calendar order (today's events first). 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. The usual rules for what comes next still apply. a piece of news or other content that should be published now or on a particular future date. you must first create an engine page for events before you can add them to your site. A blog post is an article. An event. you can begin adding upcoming events to the system. Once you've done that. As with blog posts. == Working With Events == Events are slightly different from blog posts. 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. The same features that are available for blog posts can also be used to customize the appearance and behavior of events.php` (which still must be in your application level override of the `aBlog/templates` folder). apostropheBlogPlugin provides separate back end administration and front end engine pages for events. `_slot` is still appended because were are simply requiring the blog plugin to act as if this post's template was `myTemplate`. by contrast. 132 | P a g e . 'aBlogSingle'. Events.{{{ <?php a_slot('blogpost'. array('template' => 'myTemplate') ?> }}} This forces the use of `_myTemplate_slot. on the other hand. Blog posts are traditionally presented in reverse chronological order (newest first). and separate slots for inserting single events and groups of upcoming events. has a fixed start time and end time during which it will take place. 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`.

It's not so easy to authenticate users using their system of choice and filter spam effectively. those who are members of the `editors` group will be able to write blog posts. Again. you are a potential blog author. the blog plugin looks for the same permissions that are used to determine which users are potential webpage editors. which typically becomes the public-facing "calendar of upcoming events" for your project. Admins can also grant editors the right to edit specific posts written by others. and also determine which editors are permitted to assign which categories to blog posts. a link to that event will be included in the results.When you visit the events engine page. == Managing Editing Privileges in the Blog Plugin == By default. you'll see upcoming events sorted by start date and time. which are presented in reverse chronological order. Under the hood. and delete and edit their own posts. So rather than reinvent the wheel we've provided the hooks for you to take advantage of them. 133 | P a g e . this is different from blog posts. If you search for something that appears in a post or event. That is. Those who are members of the `admin` group will be able to edit and delete the blog posts of others. In a nutshell: it's easy to implement comments badly. Third party services like Disqus do it brilliantly for free. if you are a potential webpage editor. == Adding Blog Comments with Disqus == You may wonder why we didn't implement our own system for commenting on blog posts. Users can still navigate to past events using the provided links to browse by year. month and day. == Apostrophe Search and the Blog Plugin == Good news: published blog posts and events are automatically integrated into Apostrophe's sitewide search feature.

[http://disqus. 134 | P a g e . (if you started with our sandbox. as well as in the Disqus help documents. for instance to meet the needs of those who are blogging for an intranet audience where Disqus is not an option. If you're using the 1. you will need to override a few blog templates to enable disqus.5+) site.com/ Visit Disqus and set up an account]. in that case just uncomment 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.5 we've made it incredibly easy to implement disqus in your apostrophe (1.4 instructions below. All you have to do is add this block to your project app. this is probably already in your project app. Here's how to set up Disqus with the Apostrophe 1.(At a later date we may implement our own comment system.4 blog plugin: 1.yml: aBlog: disqus_enabled: true disqus_shortname: yourdisqusshortname Just change the disqus shortname to the shortname you created when you signed up for disqus. Disqus will probably always be a desirable solution for most. There are instructions for that in the 1.4 version of apostrophe.yml.) In Apostrophe 1.

On the "Choose Install Instructions" page. add the `#disqus_thread` suffix to blog post permalinks so they can display comment counts. 4. $a_blog_post) ?> </h3> }}} 135 | P a g e . Change this: {{{ <h3 class="a-blog-item-title"> <?php echo link_to($a_blog_post->getTitle().2." 3.. as well as any blog templates you have added). and add the following inside this `if . Optionally. click "Universal Code. Copy the existing `aBlog/showSuccess`. Override the `aBlog/showSuccess` template at the app level. 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.. in your blog post template overrides (`aBlog/singleColumnTemplate` and `aBlog/twoColumnTemplate`. 'a_blog_post'.

this code is provided on the Disqus "universal code" installation instructions page.To this: {{{ <h3 class="a-blog-item-title"> <?php $url = url_for('a_blog_post'. The new code is the second line. )) ?></li> }}} 7. Paste the "embed code" provided by Disqus into your `aBlog/disqus` partial (this code is provided on the Disqus site).php`: 136 | P a g e . Add the Disqus "comment count code" just before the closing `</body>` tag of your `layout. array('class' => 'a-btn big alt'. 'http://YOURDISQUSSHORTCODE. so you should copy the latest code given there. but we've reproduced it here for your convenience. $a_blog_post) . you can also put Disqus into "developer mode" by pasting this code just before the closing `</head>` tag of your `layout. This is a Symfony admin generator partial. Note that you must replace "YOURDISQUSSHORTCODE" with '''your''' disqus short code as provided by Disqus: {{{ <?php echo $helper->linkToNew(array( 'params' => array( ). 6.com'. Provide yourself a convenient way to access the Disqus comment administration page for each blog post by overriding the `aBlogAdmin/list_actions` partial.)) ?> <li><?php echo link_to('Comments'. Again. 'class_suffix' => 'new'. 'label' => 'New'.php`. '#disqus_comments' ?> <a href="<?php echo $url ?>"><?php echo $a_blog_post->getTitle() ?></a> </h3> }}} 5. You can fetch it from your Symfony cache. Optionally.disqus.

yml`. which will then show Disqus comments.yml` file. // this would set it to developer mode </script> }}} 8. Follow the documentation on the Disqus site if you wish to style Disqus to more closely match your site's style.yml too! # Now change the comments setting comments: true }}} As with all changes to `app.yml` into the `all` section of your application-level `app.{{{ <script type="text/javascript"> var disqus_developer = 1. Turn on support for allowing comments in `app. 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. blog authors and editors will be able to check the "allow comments" box for individual posts. 137 | P a g e .yml`. After following these steps you should immediately have working Disqus comments on your site. don't forget the `symfony cc` command. Once you have done this.''' Then you can make this change: {{{ all: aBlog: # Be sure to copy the rest of the aBlog settings from the plugin app.

=== UTF-8 and Apostrophe === Apostrophe stores content in UTF-8 for compatibility with nearly all languages. However. 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. and translation of the actual site content. This section assumes familiarity with earlier sections of the manual (the Developer's Guide is optional). which your clients see. If you are editing site content in a language for which our administrative interface has not yet been translated.1 series of Apostrophe. so you can potentially translate your content into any language. you'll see an administrative interface in English. At this time the user interface used by those editing content on the site supports French.1 stable branch when it appears.x series has issues with generating slugs for items with non-Latin1 characters in their names. Spanish. German and English. the Apostrophe 1. === What Languages Are Available? === Apostrophe supports UTF-8. 138 | P a g e . Support for this is already committed in the trunk and will be standard in the 1.0. which end users of the site will see. with more languages on the way.= 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.

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:

{{{ all: doctrine: class: param: dsn: mysql:dbname=demo;host=localhost sfDoctrineDatabase

username: root password: root encoding: utf8 attributes: DEFAULT_TABLE_TYPE: INNODB DEFAULT_TABLE_CHARSET: 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

unset($_SERVER['PATH_INFO']); }}}

=== 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 project.

{{{ all: a: # 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:

{{{ all: .settings: 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 fly.

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 jQuery.)

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 twobrowsers method works very well in practice.

=== Adding the Culture to the URL ===

141 | P a g e

To add a new translation. `/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. This will allow Google to find them. "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/`. 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. This leads to the question. action: show } requirements: { slug: . Also consider adding direct links to the homepages for the other languages you support on your site. You can do this by changing your `a_page` route (beginning with Apostrophe 1. including French.In order for Google to index content in several languages you will need distinct URLs for several languages. you can take two approaches: 142 | P a g e . German. Install the sandbox project to see these in action right away. `/en/`. === Translating the User Interface Into a New Language === As with any Symfony project.5): {{{ a_page: url: /:sf_culture/:slug param: { module: a. and Spanish.* } }}} This allows Google to index separate pages for separate languages and allows links to languagespecific pages to be shared by users. You can of course copy them to your own project's `apps/frontend/i18n` folder.

Of course. and your work will benefit the entire Apostrophe open source community. we hope you will decide to share the results with the rest of the community. [http://groups. Alternatively. and get started translating. Those who are comfortable with XLIFF files may prefer this approach.1.google. [wiki:ManualImportGuide Continue to Import & Migration] [ManualOverview Up to Overview] 143 | P a g e . 2.com/group/apostrophenow Join the Apostrophe Now Google Group] and express interest in translating the user interface. just copy one of the existing XLIFF files in `apps/frontend/i18n` in our sandbox project. Our team will give you access to a convenient back end interface for translating the content. changing the language code to the appropriate 2-letter ISO code for your language.

0" encoding="UTF-8" ?> <site> <Page slug="/" title="Home" template="home" > <Area name="body"> <Slot type="aRichText"> <value>This is my body text</value> </Slot> </Area> <Area name="header_image"> 144 | P a g e ./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. The task can be run using {{{ .= 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.

com/4090/5073384290_63ea8ab19d_b. data/pages/3.flickr.jpg"></a> And another <b><i>box</i></b> ]]> </value> </Slot> 145 | P a g e .static.jpg" alt="Header"/> </Slot> </Area> <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" /> </Page> </Page> </site> }}} An example of an external file used to include slot info.gif">More regular text.0" encoding="UTF-8"?> <Page> <Area name="body"> <Slot type="foreignHtml"> <value><![CDATA[ <b>This is blod text</b><img src="food-fast.xml {{{ <?xml version="1. <a href="http://www.com"><img src="http://farm5.<Slot type="aImage"> <MediaItem src="header.punkave.

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.</Area> </Page> }}} Page elements have 4 attributes. [ManualOverview Up to the Overview] 146 | P a g e . '''The foreignHtml content type is used to import rich text with embedded images'''. and foreignHtml. xsd schema is attached to this page. aImage. If relative importer will expect file to be located at data/a/images. Currently 3 slot types are supported by the importer: aRichText. A file-id of 2 will result in the importer looking for the file data/pages/2. currently both the slug and title are required. MediaItem src attribute can either be a http:// format url or a relative url.xml. If file-id is not specified the importer will not look for areas in the main xml file. this is useful for particularly large sites where loading one XML file is not feasible. the import process will create multiple rich text and imageSlots.

In some cases a staging server is not available.yml` file must have database settings for the production server as well as for the staging server. take care to preserve the indentation as it is significant).ini for production use === The `config/databases.yml and config/properties. Examine this file to see how the staging settings are set up (it is a humanreadable configuration file. not to keep this file in svn where developers can see it. === Setting Up config/databases. 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. We share it with you to better document useful tools like `apostrophe:deploy` and `project:sync-content`. But we like and recommend them and also feel this document would benefit from third-party feedback. although our preference is naturally to use a staging server. Please note that it is not mandatory at all to use those tools with Apostrophe. in which case we deploy directly to production. for security reasons. If you prefer.The following is our internal process for deploying new Apostrophe client sites. 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 . 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. Instead we use svn to keep the code up to date on development Macs and use various Symfony tasks (based on rsync. . Contributing directly at the code level requires svn access to the project. using svn for version control and local copies of Apache and PHP to interact with the site before it is deployed. it is necessary to sync the code first from a development Mac to the staging server and then on to the production server.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. This assures that each change has been properly reviewed. Note that we do not use svn for final deployment. mysqldump and related commands) to deploy code and content as needed. So deploy the site to production.

Examine this file to see how the staging settings are set up. There are rare exceptions but it is best to discuss them with P'unk Avenue first. copy `web/index. Since this file does not contain the password there is no security risk in adding the production settings to this file. `index. The following command will push the current code from the staging server or a development Mac to the 148 | P a g e .php` file has not been set up on that server yet.php` acts as the sole "switch" that determines which settings should be used on a particular host. 'staging'.copy there. === Syncing For The First Time === When syncing code to the production server for the first time. we use the `apostrophe:deploy` Symfony task. you will receive some errors when running the `apostrophe:deploy` task. The `config/properties. 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.) After the first code sync. and edit the file so that it enables the prod environment rather than the staging environment: {{{ ProjectConfiguration::getApplicationConfiguration('frontend'. Otherwise the page tree and versioning system can be easily damaged. (Note to experienced Symfony developers: this is a departure from the default "frontend_dev.ini` file must contain ssh credentials (but no password) for the production server. This file is not synced because it is specific to each server. This is common on sites where P'unk Avenue does not have direct access to the production server. This is normal and due to the fact that the `web/index.php` from staging to production manually. }}} === Syncing Code === To sync code from one server to another.php" approach that we prefer because it simplifies debugging and deployment. true).

This is the way to deploy fixes to the code. It carries out several Symfony commands: {{{ [Locally] project:permissions project:deploy apostrophe:fix-remote-permissions [Remotely] cc doctrine:migrate apostrophe:migrate }}} 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.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. Note that the `apostrophe:fix-remote-permissions` task uses a password set in `config/properties. This is important to avoid surprising results especially with the APC cache. This command can take a long time and sometimes runs quietly./symfony apostrophe:deploy production prod }}} You will be prompted several times for the ssh password. Note that this does not affect the content on the site. only the code.production server.ini` is: {{{ 149 | P a g e . or to initially deploy the site to production: {{{ .

However. which does not introduce any great new security risk. if you are not comfortable with this. If P'unk Avenue does not have direct access to the production server then this command is carried out by the client system administrator.[sync] password=agoodveryrandompassword }}} 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. use this command: {{{ . If everything is in the database you're OK. since if PHP is compromised it is still possible to call `system()` even when the Apache user has no shell. but if you need to manage files you have a problem on your hands. To deploy new code from a development Mac to the staging server (which we recommend doing first before deploying anything to the production server). '''The best way to address this issue is to run command line tasks and Apache as the same user'''. which carries out the same steps as the `project:permissions` task. but does so as Apache./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. === Syncing Content === 150 | P a g e . it invokes the `aSync/fixPermissions` action. `apostrophe:fix-remotepermissions` is a useful workaround.

Always think about what machine you are syncing from and what machine you are syncing to. But pushing content to another server should be done with great care and caution. 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): {{{ . typed on the staging server. Measure twice. unless proper backups of the database and the web/uploads folder are being made./symfony project:sync-content frontend staging to prod@production }}} You will be prompted several times for the ssh password. cut once. There is NO way to undo this operation. as well as any other data folders specified in `app. 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. Note that the `project:sync-content` task copies both the MySQL database and the `web/uploads` and `data/a_writable` folders.yml` (usually these are the only ones). ==== Periodic Sync Back From Production for Better Testing ==== The following command. Content "lives" in production (after launch).Pushing code to the production server is normal. so it makes sense to periodically refresh the content on staging with the current content of production: 151 | P a g e . typed on the staging server. will sync content FROM the staging server TO the production server. This command can take a long time and sometimes runs quietly. will sync content FROM the production server back down TO the staging server. This should only be done ONCE when the production server is first set up. ==== One-Time Content Sync TO Production At Launch ==== The following command.

{{{ . Rebuilding the search index on a development Mac is an optional step if you are not testing searchrelated issues. 152 | P a g e ./symfony apostrophe:rebuild-search-index --env=prod }}} Similarly. Otherwise searches will not return results. The following command. you would run this command ON production to rebuild the search index: {{{ ./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 staging and use `--env=staging` if you synced content back down to staging. Always proofread this command carefully. typed on a development Mac./symfony project:sync-content frontend staging from prod@production }}} Note the use of “from” rather than “to” above. will sync content FROM the staging server back down TO the development Mac for realistic testing: {{{ .

is available in the [ManualOverview Apostrophe manual]. Code changes are typically made on a development laptop and then committed with 'svn commit. 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. "I made a change to the code on the staging or production server and someone deployed and now the change is gone. 153 | P a g e . For client technical staff interested in contributing at a designer or developer level. “Why frontend?” Symfony projects can contain several sub-applications. === 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 projects. our content management system.Rebuilding the search index does not take the site down in the meantime. 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. === Further Reading === Complete developer documentation for Apostrophe. that is the right place to start reading.' never by hotfixing files on servers.

Sign up to vote on this title
UsefulNot useful