1 | # apostrophe-pages
|
2 |
|
3 | `apostrophe-pages` adds page rendering to the [Apostrophe](http://github.com/punkave/apostrophe) content management system. The `apostrophe-pages` module makes it easy to serve pages, fetching the requested page and making its content areas available to your Nunjucks template along with any other attributes of interest.
|
4 |
|
5 | `apostrophe-pages` also provides a UI for adding pages, deleting pages and changing page settings such as the title. As part of that, `apostrophe-pages` provides functions to fetch ancestor and descendant pages to any desired depth.
|
6 |
|
7 | ## Serving Pages
|
8 |
|
9 | By default `apostrophe-pages` renders page templates found in its `views` folder depending on the `template` property of the page. You can override the location where one or all of these are found in order to provide custom templates for your project's needs.
|
10 |
|
11 | Setting up `apostrophe-pages` is easy once you have the main `apos` object. See the [apostrophe-wiki](http://github.com/punkave/apostrophe-wiki) project for a more complete sample app. In this example we want to render any URL that matches a page slug as a page:
|
12 |
|
13 | var apos = require('apostrophe')({ ..., partialPaths: [ '/views/global' ]});
|
14 | var pages = require('apostrophe-pages')({ apos: apos, templatePath: __dirname + '/views/pages' });
|
15 |
|
16 | // Your app specific routes should come first.
|
17 | app.get('/mything', function(req, res) { ... my non-Apostrophe-related stuff ... });
|
18 |
|
19 | // Now try to serve anything else as a page.
|
20 | app.get('*', pages.serve());
|
21 |
|
22 | Notice that we set partialPaths to provide global layout templates that can be `extend`ed by our page templates, and set `templatePath` to specify where our page templates area.
|
23 |
|
24 | ## Page Types
|
25 |
|
26 | The constructor for the pages module accepts a `types` option. In its simplest form, a "page type" is just a template name and a label. If you do not specify a `types` parameter, you get a single type with the name `default` and the label `Default`. this causes `views/default.html` to be loaded to render pages with that type.
|
27 |
|
28 | However page types can also be extended with custom behavior. See the `apostrophe-blog` module for an example with all the trimmings.
|
29 |
|
30 | To facilitate code reuse, page types can have a single "superclass" from which they inherit behavior in both the server- and browser-side JavaScript code.
|
31 |
|
32 | Here is a simple example of specifying the `types` option:
|
33 |
|
34 | types: [ { name: 'default', label: 'Default (Two Column)' }, { name: 'onecolumn', label: 'One Column' }]
|
35 |
|
36 | You can also add types later, for instance when initializing other modules such as our blog module. See `pages.addType`.
|
37 |
|
38 | `pages.serve` has options that can be used to override its behavior in many ways. Complete documentation for the `pages.serve` function is provided at the top of the function in `index.js` (TODO: work on publishing this as jsdoc).
|
39 |
|
40 | Your page templates will want to render areas. Just use the page objects passed to you to access them and call the aposArea helper available in anything rendered via apos.partial, which includes page templates:
|
41 |
|
42 | {{ aposArea(page, 'main') }}
|
43 | {{ aposArea(global, 'footer') }}
|
44 |
|
45 | ## Building Navigation: Ancestors and Descendants
|
46 |
|
47 | Pages may have a path, separate from their slug, which expresses
|
48 | their relationship to other pages. Paths look like this:
|
49 | this:
|
50 |
|
51 | home
|
52 | home/about
|
53 | home/about/staff
|
54 |
|
55 | The path always reflects the relationship between the pages no
|
56 | matter what the slug may be changed to by the user. (Clients routinely
|
57 | shorten slugs to make URLs easier to publish in print, but don't want to
|
58 | lose the relationships between pages.)
|
59 |
|
60 | Note that paths do not have a leading /. Multiple roots are permitted,
|
61 | but typically there is one root page with the path `home`.
|
62 |
|
63 | Pages also have a rank, which determines their ordering among
|
64 | the children of a particular page.
|
65 |
|
66 | ### Fetching Ancestors and Descendants Manually ###
|
67 |
|
68 | `pages.getAncestors` and `pages.getDescendants` can be used to fetch
|
69 | the ancestors and descendants of a page. `pages.getAncestors(req, page, callback)` delivers
|
70 | ancestor pages to its callback, in order beginning with the root page.
|
71 | `pages.getDescendants(req, page, callback)` delivers the children of the page, in order by rank. `req` must be passed to provide a context for permissions (`req.user`) and, potentially, for caching during the lifetime of a single request. It must be either a real Express request object or some other object acceptable to your `apos.permissions` function.
|
72 |
|
73 | You can optionally specify a depth:
|
74 |
|
75 | `pages.getDescendants(req, page, { depth: 2 }, callback)`
|
76 |
|
77 | In this case your callback still receives an array of the immediate children of `page`. However, each of those pages has a `children` property containing an array of its children.
|
78 |
|
79 | For performance reasons, `pages.getAncestors` and `pages.getDescendants` do not return the `items` property. Typically only the `slug` and `title` properties are necessary to build navigation. If necessary you may use the slug property of a page to fetch the entire page with its items, via `apos.getPage`.
|
80 |
|
81 | ### Fetching Ancestors, Peers and Descendants Automatically ###
|
82 |
|
83 | `pages.serve` automatically fetches the ancestors of the page into the `ancestors` property of the `page` object given to the page template. In addition, the children of the page are available in the `children` property. And the children of the home page (whether the current page is the home page or not) are available in the `tabs` property. Also, the peers of the current page (children of the same parent) are available in the `peers` property.
|
84 |
|
85 | If you need to see the descendants of the current page to a greater depth, set the `descendantOptions` option when calling `pages.serve`:
|
86 |
|
87 | {
|
88 | descendantOptions: {
|
89 | depth: 2
|
90 | }
|
91 | }
|
92 |
|
93 | You can then look at the `children` property of each entry in `page.children`, and so on.
|
94 |
|
95 | To do the same thing for descendants of the home page, set:
|
96 |
|
97 | {
|
98 | tabOptions: {
|
99 | depth: 2
|
100 | }
|
101 | }
|
102 |
|
103 | You can add a `.children` array to each ancestor in order to implement accordion navigation, in which each ancestor's children are also displayed:
|
104 |
|
105 | {
|
106 | ancestorOptions: {
|
107 | children: true
|
108 | }
|
109 | }
|
110 |
|
111 |
|
112 | You can also shut off ancestors, descendants or tabs entirely if you're not interested:
|
113 |
|
114 | {
|
115 | ancestors: false
|
116 | tabs: false,
|
117 | descendants: false,
|
118 | }
|
119 |
|
120 | ### Fetching Pages by Tag ###
|
121 |
|
122 | Typically "tree pages" (pages that are part of the tree, i.e. the home page and its descendants) are displayed and browsed like nested folders. But from time to time it is useful to fetch pages based on taxonomy instead.
|
123 |
|
124 | The `pages.getByTag` method returns *all tree pages on the site* that have a specified tag:
|
125 |
|
126 | pages.getByTag(req, 'green', function(err, results) { ... });
|
127 |
|
128 | And `pages.getByTags` returns all pages with at least one of the tags in an array:
|
129 |
|
130 | pages.getByTags(req, ['green', 'blue'], function(err, results) { ... });
|
131 |
|
132 | When pages are fetched by tag, they are sorted alphabetically by title.
|
133 |
|
134 | ### Filtering Pages by Tag ###
|
135 |
|
136 | If you already have an array of pages, for instance the children or peers of the current page, it can be useful to filter them by tag. You can do that with `pages.filterByTag`:
|
137 |
|
138 | pages.filterByTag(children, 'tag')
|
139 | pages.filterByTags(children, ['green', 'blue'])
|
140 |
|
141 | Again, `filterByTags` returns pages with *at least one* of the specified tags.
|
142 |
|
143 | *Note that these functions do not take a callback.* They return the pages directly, since they are just filtering an existing array of pages based on their metadata.
|
144 |
|
145 | ## Loading Additional Data
|
146 |
|
147 | Most sites require that some extra data be loaded along with pages. The data to be loaded is often dependent on the site structure and the unique needs of the project. The pages.serve function supports this via the load option.
|
148 |
|
149 | The load option accepts an array made up of page slugs and functions. Any strings found in this array are assumed to be the slugs of pages, usually "virtual pages" whose slugs do not start with a leading / and are not directly reachable by navigating the site. Such pages are loaded and added to the `req.extras` property. All properties of `req.extras` are then made available to your page templates. So if you use the virtual page `global` to hold a shared global footer area, you can access it as `global.footer` in your page templates.
|
150 |
|
151 | In addition, loaders can be asynchronous functions that modify the `req` object in their own ways. Loaders receive the `req` object as their first parameter and a callback to be invoked on completion as their second parameter. The `req` object will have a `page` property containing the page that matched the slug, if any, and a `remainder` property matching additional content in the URL after the slug if the page is greedy, as explained below.
|
152 |
|
153 | ## Second Chances and "Greedy Pages"
|
154 |
|
155 | Many sites need to go beyond a simple tree of pages, implementing experiences like blogs and catalogs that require "subpages" to exist for every product or blog post, and URLs that contain elements other than page slugs, such as the date and slug of a blog post. This is easily implemented using greedy pages.
|
156 |
|
157 | While the `req.page` page property is set only if the slug exactly matches a page, the `req.bestPage` property is set to the page whose slug comes closest to matching the page. To be the "best page," a page must meet the following conditions:
|
158 |
|
159 | Either (1) it matches the requested URL exactly (in which case req.page and req.bestPage will be the same page), or (2) it matches the longest portion of the URL ending in a `/`.
|
160 |
|
161 | Examples:
|
162 |
|
163 | Assume there are pages with the slugs `/blog` and `/blog/credits` in the databsae.
|
164 |
|
165 | 1. If the user requests `/blog` and there is a page with the slug `/blog`, both `req.page` and `req.bestPage` will be set to the `/blog` page object. `req.remainder` will be the empty string.
|
166 | 2. If the user requests `/blog/2013/01/01/i-like-kittens`, `req.page` will be undefined, and `req.bestPage` will be set to the `/blog` page object. `req.remainder` will be set to `/2013/01/01/i-like-kittens`.
|
167 | 3. If the user requests `/blog/credits`, `req.page` and `req.bestPage` will both be set to the `/blog/credits` page object. `req.remainder` will be the empty string.
|
168 | 4. If the user requests `/blog/credits/paul`, `req.page` will be undefined, and `req.bestPage` will be set to the `/blog/credits` page object. `req.remainder` will be set to `/paul`.
|
169 | 5. For consistency, if the URL ends with a trailing /, this is not included in `req.remainder`.
|
170 |
|
171 | This approach allows a mix of "ordinary" subpages and subpages implemented by custom logic in a `load` function that examines `req.remainder` and `req.bestPage` to decide what to do.
|
172 |
|
173 | ### Converting req.bestPage to req.page ###
|
174 |
|
175 | A loader that decides a page should be rendered after all based on a partial match should set `req.page` to `req.bestPage`. Otherwise the page is considered to be a 404.
|
176 |
|
177 | ### Switching Templates In A Load Function ###
|
178 |
|
179 | You could implement a blog with custom behavior for different values of `remainder` entirely by setting properties of `req.extras` and examining them in your template.
|
180 |
|
181 | But it is often easier to use an entirely different template, for instance to render a blog post's permalink page differently from the main index of a blog.
|
182 |
|
183 | To achieve that in your `load` function, just set `req.type` to the template you want to render:
|
184 |
|
185 | `req.type = 'blogPost';`
|
186 |
|
187 | Note that you can set `req.type` to `notfound` to display the standard "404 not found" template for the project.
|
188 |
|
189 | ## User Interface: Adding, Modifying and Removing Pages ##
|
190 |
|
191 | `apostrophe-pages` provides a full user interface for creating, modifying and removing pages. To enable it, just insert the appropriate markup into your page layout:
|
192 |
|
193 | {{ aposEditPage({ page: page, edit: edit, root: '/' }) }}
|
194 |
|
195 | This helper function inserts the page-related buttons at that point and also the necessary browser-side JavaScript to power them.
|
196 |
|
197 | ## Automatic Redirects ##
|
198 |
|
199 | If you change the slug (URL) of a page via the Page Settings button, that doesn't tell Google and other search engines that the page has moved. So as a convenience, `apostrophe-pages` automatically tracks the old URLs and provides redirects to the new URLs. Of course, if a new page is created at the old URL, that page wins and the old redirect is not used.
|
200 |
|
201 | ## Search
|
202 |
|
203 | The `apostrophe-search` module also provides a sitewide search facility. This is implemented by a page loader function that kicks in for pages (usually just one) with the `search` type.
|
204 |
|
205 | Simple filters are provided to include or exclude results. There is a checkbox for each searchable instance type (such as `blogPost` or `event`) and for regular pages. You can override these with the `searchLoader` option to the pages module. For instance, in `app.js` in a project using `apostrophe-site` for configuration:
|
206 |
|
207 | ```javascript
|
208 | pages: {
|
209 | searchLoader: [
|
210 | {
|
211 | name: 'page',
|
212 | label: 'Pages'
|
213 | },
|
214 | {
|
215 | name: 'blogPost',
|
216 | label: 'Blog Posts'
|
217 | }
|
218 | ]
|
219 | }
|
220 | ```
|
221 |
|
222 | Any searchable document whose page type does not have a specific filter is toggled by the `page` filter. *If you do not include a filter with the name `page` then such documents will not be visible in search results.* However this is frontend filtering and should not be relied upon to secure information by keeping it out of search. For that, if you are subclassing snippets, you may use the `searchable` option when configuring the module.
|
223 |
|
224 | ## Context Menu
|
225 |
|
226 | Apostrophe's "page menu" in the lower left corner is designed to be context-sensitive. Its contents can be overridden for the current page by setting `req.contextMenu` in a page loader function.
|
227 |
|
228 | Here's an example:
|
229 |
|
230 | ```javascript
|
231 | req.contextMenu = [
|
232 | {
|
233 | name: 'new-monkey',
|
234 | label: 'New Monkey'
|
235 | },
|
236 | {
|
237 | name: 'edit-page',
|
238 | label: 'Page Settings'
|
239 | },
|
240 | {
|
241 | name: 'versions-page',
|
242 | label: 'Page Versions'
|
243 | },
|
244 | {
|
245 | name: 'delete-page',
|
246 | label: 'Move to Trash'
|
247 | }
|
248 | {
|
249 | name: 'reorganize-page',
|
250 | label: 'Reorganize'
|
251 | }
|
252 | ]
|
253 | ```
|
254 |
|
255 | Note that if we set this property, any options we don't spell out will NOT appear. So you can use this option to hide standard choices like "Reorganize."
|
256 |
|
257 | The standard choices are as shown above beginning with `edit-page`.
|
258 |
|
259 | "How do I implement my `new-monkey` option?"
|
260 |
|
261 | The context menu outputs links with `data` attributes based on the `name` property. The links also get classes based on this property as you can see via "inspect element" in your browser.
|
262 |
|
263 | Consider this browser-side JavaScript, which invites the user to create a new page of type `monkey` when the above link is clicked:
|
264 |
|
265 | ```javascript
|
266 | $('body').on('click', '[data-new-monkey]', function() {
|
267 | // Get metadata about the current page we're looking at
|
268 | var page = apos.data.aposPages.page;
|
269 |
|
270 | // Grab the title of the link for use at the top of the modal dialog
|
271 | var title = $(this).text();
|
272 | // Pop open the "new page" dialog with the page type set to
|
273 | // "monkey"
|
274 | var $el = aposPages.newPage(page.slug, { type: 'monkey', title: title });
|
275 | return false;
|
276 | });
|
277 | ```
|