UNPKG

15.1 kBMarkdownView Raw
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
9By 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
11Setting 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
22Notice 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
26The 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
28However page types can also be extended with custom behavior. See the `apostrophe-blog` module for an example with all the trimmings.
29
30To 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
32Here is a simple example of specifying the `types` option:
33
34 types: [ { name: 'default', label: 'Default (Two Column)' }, { name: 'onecolumn', label: 'One Column' }]
35
36You 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
40Your 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
47Pages may have a path, separate from their slug, which expresses
48their relationship to other pages. Paths look like this:
49this:
50
51 home
52 home/about
53 home/about/staff
54
55The path always reflects the relationship between the pages no
56matter what the slug may be changed to by the user. (Clients routinely
57shorten slugs to make URLs easier to publish in print, but don't want to
58lose the relationships between pages.)
59
60Note that paths do not have a leading /. Multiple roots are permitted,
61but typically there is one root page with the path `home`.
62
63Pages also have a rank, which determines their ordering among
64the children of a particular page.
65
66### Fetching Ancestors and Descendants Manually ###
67
68`pages.getAncestors` and `pages.getDescendants` can be used to fetch
69the ancestors and descendants of a page. `pages.getAncestors(req, page, callback)` delivers
70ancestor 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
73You can optionally specify a depth:
74
75`pages.getDescendants(req, page, { depth: 2 }, callback)`
76
77In 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
79For 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
85If 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
93You can then look at the `children` property of each entry in `page.children`, and so on.
94
95To do the same thing for descendants of the home page, set:
96
97 {
98 tabOptions: {
99 depth: 2
100 }
101 }
102
103You 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
112You 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
122Typically "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
124The `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
128And `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
132When pages are fetched by tag, they are sorted alphabetically by title.
133
134### Filtering Pages by Tag ###
135
136If 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
141Again, `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
147Most 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
149The 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
151In 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
155Many 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
157While 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
159Either (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
161Examples:
162
163Assume there are pages with the slugs `/blog` and `/blog/credits` in the databsae.
164
1651. 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.
1662. 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`.
1673. 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.
1684. 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`.
1695. For consistency, if the URL ends with a trailing /, this is not included in `req.remainder`.
170
171This 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
175A 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
179You 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
181But 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
183To achieve that in your `load` function, just set `req.type` to the template you want to render:
184
185`req.type = 'blogPost';`
186
187Note 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
195This 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
199If 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
203The `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
205Simple 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
208pages: {
209 searchLoader: [
210 {
211 name: 'page',
212 label: 'Pages'
213 },
214 {
215 name: 'blogPost',
216 label: 'Blog Posts'
217 }
218 ]
219}
220```
221
222Any 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
226Apostrophe'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
228Here's an example:
229
230```javascript
231req.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
255Note 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
257The standard choices are as shown above beginning with `edit-page`.
258
259"How do I implement my `new-monkey` option?"
260
261The 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
263Consider 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```