# apostrophe-pages
# Inherits from: apostrophe-module
# apos.pages
This module manages the page tree and contains the wildcard
Express route that actually serves pages. That route is installed
at the very end of the process, in an afterInit
callback,
which is late enough to allow all other modules to add routes first.
Also implements parked pages, "plain old page types" (those that aren't powered by a module), the context menu (big gear menu) and the publish menu.
# Options
# types
Specifies the page types that can be chosen for a page.** This list must include all page types that will be present in the tree (not piece types).
The default setting is:
types: [
{
name: 'home',
label: 'Home'
},
{
name: 'default',
label: 'Default'
}
]
The home
page type is required.
# allowedHomepageTypes
An array of page type names that are permitted for
the homepage. This should be a subset of the types that appear in the
types
option. Example:
allowedHomepageTypes: [ 'home' ]
If this option is not specified, the homepage may be switched to any type
present in types
.
# allowedSubpageTypes
An array of page type names that are allowed when adding a subpage of a page of each type. If this array is empty, you cannot add a subpage to a page of that type. Example:
allowedSubpageTypes: {
home: [ 'default', 'blog-page' ],
default: [ 'grandchild' ],
grandchild: []
}
If subpages are not specified for a type, then it may have subpages
of any type present in types
.
# contextMenu
Specifies the default offerings on the context menu.** These
can also be overridden for any request by setting req.contextMenu
to an array
in the same format.
The default setting is:
contextMenu: [
{
action: 'insert-page',
label: 'New Page'
},
{
action: 'update-page',
label: 'Page Settings'
},
{
action: 'versions-page',
label: 'Page Versions'
},
{
action: 'trash-page',
label: 'Move to Trash'
},
{
action: 'reorganize-page',
label: 'Reorganize',
// Until we port the provisions for non-admins to reorganize
// over from 0.5
permission: 'admin'
}
]
The action
becomes a data-apos-ACTIONGOESHERE
attribute on the
menu item. If permission
is set, the item is only shown to users
with that permission (this is NOT sufficient protection for the
backend routes it may access, they must also be secured).
# publishMenu
Configures the publication menu,** which appears
only if the current page is unpublished or data.pieces
is present
and is unpublished. Syntax is identical to contextMenu
. The default
setting is:
publishMenu: [
{
action: 'publish-page',
label: 'Publish Page'
}
]
Again, you can override it by setting req.publishMenu
.
If you are looking for the schema fields common to all pages in the tree, check out the apostrophe-custom-pages module, which all page types extend, including "ordinary" pages.
# park
Configures certain pages to be automatically created and refreshed
whenever the site starts up.** The parked pages you get are actually the
concatenation of the minimumPark
and park
options.
minimumPark
has a default, which you will typically leave unchanged:
[
{
slug: '/',
published: true,
_defaults: {
title: 'Home',
type: 'home'
},
_children: [
{
slug: '/trash',
type: 'trash',
trash: true,
published: false,
orphan: true,
_defaults: {
title: 'Trash'
},
}
]
},
]
The
park
andminimumPark
options are arrays. Each array is a page to be created or recreated on startup.If a page has a
parent
property, it is created as a child of the page whoseslug
property equalsparent
. If a page has noparent
property and it is not the home page itself, it is created as a child of the home page.Any other properties that do not begin with a
_
are automatically refreshed on the page object in the database at startup time.RECOMMENDED: give every parked page a
parkedId
property which is unique among your parked pages. If you do this, you will be able to change the slug property later. If you don't, changing the slug property will result in two pages, because it is being used to identify the existing parked page. You MAY add this property later, but you MUST DO IT BEFORE you changeslug
(not at the same time).If a page has a
_children
array property, these are additional parked pages, created as children of the page.The properties of the
_default
option are applied to the page object only at creation time, meaning that changes users make to them later will stick.orphan: true
prevents a page from appearing in standard navigation links based on parent-child relationships (as opposed to hand-built navigation widgets powered by joins and the like).The "page settings" UI is evolving toward not allowing users to modify properties that are explicitly set via
park
(rather than being set once via_defaults
). In any case such properties are reset by restarts.
# filters
Apostrophe cursor filters applied when fetching the current page.**
The default settings ensure that req.data.page
has a _children
property
and an _ancestors
property:
{
// Get the kids of the ancestors too so we can do tabs and accordion nav
ancestors: { children: true },
// Get our own kids
children: true
};
See the apostrophe-pages-cursor type for additional
cursor filters and options you might wish to configure, such as adding
a depth
option to children
.
# home
Apostrophe populates the home page from req.page._ancestors[0]
if possible.
If not, Apostrophe fetches the home page separately, using the same filters configured for
ancestors. You can shut this extra query off:
{
home: false
}
In addition, if ancestors are not configured, Apostrophe assumes you want the children of the home page. You can shut that off, and still get the home page:
{
home: {
children: false
}
}
# deleteFromTrash
If set to true
, Apostrophe offers a button in the
"reorganize" view to permanently delete pages that are already in the trash.**
This option defaults to false
because, in our experience, customers usually
ask for a way to "un-empty the trash," and of course there isn't one. We don't
recommend enabling the feature on a permanent basis but it can be useful during
the early stages of site population.
# infoProjection
A MongoDB-style projection object indicating which properties of a page will be returned
by the info
web API used to refresh information about a page after an editing operation
in the reorganize view. This was added for security reasons. You probably will not need
to expand this unless you are overriding the reorganize view code heavily.
# Methods
# pushAssets() [browser]
# getCreateSingletonOptions(req) [browser]
# createRoutes() [routes]
# find(req, criteria, projection) [api]
Obtain a cursor for finding pages. Adds filters useful for including ancestors, descendants, etc.
# findForBatch(req, criteria, projection) [api]
Returns a cursor that finds pages the current user can edit in a batch operation, including unpublished and trashed pages.
# insert(req, parentOrId, page, options, callback) [api]
Insert a page as a child of the specified page or page ID.
The options
argument may be omitted completely. If
options.permissions
is set to false, permissions checks
are bypassed.
If options.skipAttachments
is true, the operation will be slightly
faster, however this is only safe to use if both the schema of the document
and the schemas of any arrays and widgets within the document and its
areas contain no attachments. This does not include attachments
reached via joins.
If no callback is supplied, a promise is returned.
# withLock(req, fn) [api]
Takes a function, fn
, which expects a callback and performs
some operation on the page tree. Returns a new function that
does exactly the same thing, but obtains a lock first and
releases it afterwards.
Nested locks for the same req
are permitted, in order to allow
inserts or moves that are triggered by afterMove
, beforeInsert
, etc.
If fn passes a second argument to its callback, that argument is passed on.
# lock(req, callback) [api]
Lock the page tree.
The lock must be released by calling the unlock
method.
It is usually best to use the withLock
method instead, to
invoke a function of your own while the lock is in your
possession, so you don't have to keep track of it.
Nested locks are permitted for the same req
.
# unlock(req, callback) [api]
Release a page tree lock obtained with the lock
method.
Note that it is safest to use the withLock
method to avoid
the bookkeeping of calling either lock
or unlock
yourself.
# docAfterDenormalizePermissions(req, page, options, callback) [api]
This method pushes a page's permissions to its subpages selectively based on
whether the applyToSubpages action was selected. It also copies
the loginRequired
property to subpages in that situation.
Both additions and deletions from the permissions list can be propagated in this way.
This requires some tricky mongo work to do it efficiently, especially since we need to update both the join ids and the denormalized docPermissions array.
The applyToSubpages choice is actually a one-time action, not a permanently remembered setting, so the setting itself is cleared afterwards by this method.
If 'appendPermissionsToSubpages' option is selected then the new set of permissions is appended to the existing subpage's permissions instead of completly overriding them. Thus preserving any special permissions given to a subfolder or a subpage, while adding the new ones to them. Like 'applytoSubpages' choice, 'appendPermissionsToSubpages' is also a one-time action, so the setting itself is cleared afterwards by this method.
This method is called for us by the apostrophe-docs module on update operations, so we first make sure it's a page. We also make sure it's not a new page (no kids to propagate anything to).
# newChild(parentPage, type) [api]
This method creates a new object suitable to be inserted as a child of the specified parent via insert(). It DOES NOT insert it at this time. If the parent page is locked down such that no child page types are permitted, this method returns null. The permissions of the new child page match the permissions of the parent.
# allowedChildTypes(page) [api]
Return an array of child page type names permitted given the specified parent page. If page is null, allowable type names for the home page are returned.
# isAllowedChildType(page, type) [api]
Return true if the given type name is allowable for a child of the given page. If page is null, this method returns true if the given type name is allowable for the home page.
# move(req, movedId, targetId, position, options, callback) [api]
Move a page already in the page tree to another location.
position can be 'before', 'after' or 'inside' and determines the moved page's new relationship to the target page.
The callback receives an error and, if there is no error, also an array of objects with _id and slug properties, indicating the new slugs of all modified pages.
Less commonly used features
These are mainly for use by modules that extend Apostrophe's model layer,
such as apostrophe-workflow
.
The options
argument may be omitted entirely.
If options.criteria
is present, it is merged with
all MongoDB criteria used to read and write the database in self.move
.
If options.filters
is present, those filters are invoked
on any Apostrophe cursor find() calls used to read and write the database in self.move
.
In addition, options
is passed back to the callback as a third argument,
which is useful to detect recursive scenarios that come up in the
workflow module.
options
is also passed back to the movePermissions
method,
and passed as the options
property of the info
parameter of afterMove
.
After the moved and target pages are fetched, the beforeMove
method is invoked with
req, moved, target, position, options
and an optional callback.
beforeMove
may safely modify top-level properties of options
without an impact
beyond the exit of the current self.move
call. If modifying deeper properties, clone them.
If callback
is omitted, returns a promise.
# movePermissions(req, moved, data, options, callback) [api]
Based on req
, moved
, data.moved
, data.oldParent
and data.parent
, decide whether
this move should be permitted. If it should not be, throw an error.
This is invoked with callAll
, so other methods may implement it and
may optionally take a callback as a second argument, in which case errors
should be passed to the callback rather than thrown.
options
is the same options object that was passed to self.move
, or an empty object
if none was passed.
# beforeMove(req, moved, target, position, options, callback) [api]
Override this method to alter the options
object before
the move
method carries out a move in the page tree
# afterMove(req, moved, info, callback) [api]
Invoked after a page is moved. Override to carry out aditional actions
# moveToTrash(req, _id, callback) [api]
Accepts req
, _id
and callback
.
Delivers err
, parentSlug
(the slug of the page's
former parent), and changed
(an array of objects with
_id and slug properties, including all subpages that
had to move too). If the trashInSchema: true
option was
set for the module, parentSlug
is still provided
although the parent does not change, and changed
is
still provided although the slugs of the descendants
do not change.
# trashInSchema(req, _id, toTrash, callback) [api]
"Move" a page to the trash by just setting its trash flag
and keeping it under the same parent. Called by moveToTrash
when the trashInSchema
flag is in effect. The home page
still cannot be moved to the trash even in this mode.
Trashes descendant pages as well.
See moveToTrash
for what the callback receives.
# deduplicatePages(req, pages, toTrash, callback) [api]
# rescueInTree(req, _id, callback) [api]
Rescue a page previously trashed via trashInSchema
. This is an operation that only
makes sense when the trashInSchema
option flag is set for the module.
Rescues descendants as well. Invokes its callback with (null, parentSlug, changed)
,
where:
parentSlug
is the slug of the parent of the page rescued, for consistency
with the moveToTrash
method, although the parent does not change;
changed
is an array of descendant pages whose trash status also changed,
with _id
and slug
properties.
# moveToSharedTrash(req, _id, callback) [api]
Implements moveToTrash
when trashInSchema
is false (the default),
by moving the page inside the trashcan page. See moveToTrash
for what the callback receives.
# deleteFromTrash(req, _id, callback) [api]
Empty the trash (destroy a page in the trash permanently).
Currently you must specify the _id of a single page, however if it has descendants they are also destroyed.
If the page does not exist or is not in the trash an error is reported.
Delivers (err, parentSlug) to the callback.
# update(req, page, options, callback) [api]
Update a page. The options
argument may be omitted entirely.
if it is present and options.permissions
is set to false
,
permissions are not checked.
If options.skipAttachments
is true, the operation will be slightly
faster, however this is only safe to use if both the schema of the document
and the schemas of any arrays and widgets within the document and its
areas contain no attachments. This does not include attachments
reached via joins.
# park(pageOrPages) [api]
Ensure the existence of a page or array of pages and lock them in place in the page tree.
The slug
property must be set. The parent
property may be set to the slug of the intended
parent page, which must also be parked. If you
do not set parent
, the page is assumed to be a
child of the home page, which is always parked.
See also the park
option; typically invoked via
that option when configuring the module.
# serve(req, res) [api]
Route that serves pages. See afterInit in index.js for the wildcard argument and the app.get call
# serveGetPage(req, callback) [api]
# removeTrailingSlugSlashes(req, slug) [api]
Remove trailing slashes from a slug. This is factored out so that it can be overridden, for instance by the apostrophe-workflow module.
# serveLoaders(req, callback) [api]
# serveNotFound(req, callback) [api]
# serveDeliver(req, err) [api]
# pageBeforeSend(req, callback) [api]
This method invokes pushCreateSingleton
to create the apostrophe-pages
browser-side object with information about the current page, and also
sets req.data.home
. It is called automatically every
time self.sendPage
is called in any module, which includes normal CMS pages,
404 pages, the login page, etc.
This allows non-CMS pages like /login
to "see" data.home
and data.home._children
in their templates.
For performance, if req.data.page is already set and it contains a
req.data._ancestors[0]._children
property, that information
is leveraged to avoid redundant queries. If not, a query is made.
For consistency, the home page is always retrieved using the same filters that
are configured for ancestors
. Normally that includes children of each
ancestor. If that is explicitly reconfigured without the children
option,
you will not get data.home._children
.
# isFound(req) [api]
A request is "found" if it should not be treated as a "404 not found" situation
# getServePageFilters() [api]
# matchPageAndPrefixes(cursor, slug) [api]
# evaluatePageMatch(req) [api]
# ensureIndexes(callback) [api]
# ensurePathIndex(callback) [api]
# getPathIndexParams() [api]
# ensureLevelRankIndex(callback) [api]
# getLevelRankIndexParams() [api]
# pruneCurrentPageForBrowser(page) [api]
A limited subset of page properties is pushed to browser-side JavaScript. If you want more you should make your own req.browserCalls or override this method. Don't push gigantic joins if you don't want slow pages
# docFixUniqueError(req, doc) [api]
Invoked via callForAll in the docs module
# updateDescendantsAfterMove(req, page, originalPath, originalSlug, options, callback) [api]
Update the paths and slugs of descendant pages, changing slugs only if they were compatible with the original slug. Also update the level of descendants.
On success, invokes callback with null and an array of objects with _id and slug properties, indicating the new slugs for any objects that were modified.
# manageOrphans(callback) [api]
# implementParkAll(callback) [api]
# implementParkOne(req, item, callback) [api]
# unparkTask(callback) [api]
# mapMongoIdToJqtreeId(changed) [api]
Routes use this to convert _id to id for the convenience of jqtree
# docUnversionedFields(req, doc, fields) [api]
Invoked by the apostrophe-versions module.
Your module can add additional doc properties that should never be rolled back by pushing
them onto the fields
array.
# isPage(doc) [api]
Returns true if the doc is a page in the tree (it has a slug with a leading /).
# matchDescendants(page) [api]
Returns a regular expression to match the path
property of the descendants of the given page,
but not itself
# isAncestorOf(possibleAncestorPage, ofPage) [api]
Returns true if possibleAncestorPage
is an ancestor of ofPage
.
A page is not its own ancestor. If either object is missing or
has no path property, false is returned.
# beforeSave(req, page, options, callback) [api]
Invoked just before a save operation (either insert or update) on a page is actually pushed to the database. Initially empty for your overriding convenience.
# beforeInsert(req, page, options, callback) [api]
Invoked just before an insert operation on a page is actually pushed to the database. Initially empty for your overriding convenience.
# beforeUpdate(req, page, options, callback) [api]
Invoked just before an update operation on a page (not an insert) is actually pushed to the database. Initially empty for your overriding convenience.
# addApplyToSubpagesToSchema(schema) [api]
While it's a good thing that all docs now can have nuanced permissions, only pages care about "apply to subpages" as a concept when editing permissions. This method adds those nuances to the permissions-related schema fields. Called by the update routes (for new pages, there are no subpages to apply things to yet). Returns a new schema
# registerGenericPageTypes(callback) [api]
Registers a manager for every page type that doesn't already have one via apostrophe-custom-pages
,
apostrophe-pieces-pages
, etc. Invoked by modulesReady
# getParkedTypes() [api]
Get the page type names for all the parked pages, including parked children, recursively.
# registerGenericPageType(type, callback) [api]
Registers a manager for a specific page type that doesn't already have one via apostrophe-custom-pages
,
apostrophe-pieces-pages
, etc. Invoked by modulesReady
via registerGenericPageTypes
and
manageOrphans
# registerTrashPageType(callback) [api]
# validateTypeChoices() [api]
# addAfterContextMenu(helper) [api]
bc wrapper for apos.templates.append('contextMenu', helper)
.
# finalizeControls() [api]
# addPermissions() [api]
# removeParkedPropertiesFromSchema(page, schema) [api]
# removeSlugFromHomepageSchema(page, schema) [api]
any slug
field named slug
. If not, return the schema unmodified.
# getCreateControls(req) [api]
# getEditControls(req) [api]
# addToAdminBar() [api]
# getBaseUrl(req) [api]
Returns the effective base URL for the given request.
If Apostrophe's top-level baseUrl
option or APOS_BASE_URL
environment variable is set, it is returned,
otherwise the empty string. This makes it easier to build absolute
URLs (when baseUrl
is configured), or to harmlessly prepend
the empty string (when it is not configured). The
Apostrophe cursors used to fetch Apostrophe pages
consult this method, and it is extended by the optional
apostrophe-workflow
module to create correct absolute URLs
for specific locales.
# batchSimpleRoute(req, name, change) [api]
Implements a simple batch operation like publish or unpublish.
Pass req
, the name
of a configured batch operation, and
and a function that accepts (req, page, data, callback),
performs the modification on that one page (including calling
update
if appropriate), and invokes its callback.
data
is an object containing any schema fields specified
for the batch operation. If there is no schema it will be
an empty object.
If req.body.job
is truthy, replies immediately to the request with
{ status: 'ok', jobId: 'cxxxx' }
. The jobId
can then
be passed to apos.modules['apostrophe-jobs'].start()
on the rowser side to
monitor progress.
Otherwise, replies to the request with { status: 'ok', data: page }
on success. If ids
rather than _id
were specified,
data
is an empty object.
To avoid RAM issues with very large selections and ensure that lifecycle callbacks like beforeUpdate, etc. are invoked, the current implementation processes the pages in series.
# allowedSchema(req, page, parentPage) [api]
Given a page and its parent (if any), returns a schema that is filtered appropriately to that page's type, taking into account whether the page is new and the parent's allowed subpage types
# requireEditor(req, res, next) [api]
User must have some editing privileges for this type
# getInfoProjection(req) [api]
Used to fetch the projection used for the /modules/apostrophe-pages/info route to avoid disclosing
excessive information. By default, returns the infoProjection
option. A good extension point;
be sure to apply the super
pattern to get the benefit of extensions in other modules,
like workflow.
# setInfoProjection(req, cursor) [api]
Implements setting the projection for the info route, see getInfoProjection.
# modulesReady(callback)
When all modules are ready, invoke registerGenericPageTypes
to register a manager
for any page type that doesn't already have one via apostrophe-custom-pages
,
apostrophe-pieces-pages
, etc.
# afterInit(callback)
Wait until the last possible moment to add the wildcard route for serving pages, so that other routes are not blocked
# Nunjucks template helpers
# menu(options)
# publishMenu(options)
# isAncestorOf(possibleAncestorPage, ofPage)
# afterContextMenu()
# createControls()
Emit controls section of page create modal: the cancel/save buttons, etc.
# editControls()
Emit controls section of page editor modal: the cancel/save buttons, etc.
# isPage(page)
# API Routes
# POST /modules/apostrophe-pages/reorganize
# POST /modules/apostrophe-pages/publish
Implement the publish route, which can publish
one page (via req.body._id) or many (via req.body.ids).
The data
property of the API response will contain the page
only for the req.body._id
case.
# POST /modules/apostrophe-pages/unpublish
Implement the unpublish route, which can publish
one page (via req.body._id) or many (via req.body.ids).
The data
property of the API response will contain the page
only for the req.body._id
case.
# POST /modules/apostrophe-pages/tag
Implement the tag route, which can tag
one page (via req.body._id
) or many (via req.body.ids
).
The tags to be added are in the req.body.tags
array.
# POST /modules/apostrophe-pages/untag
Implement the untag route, which can untag
one page (via req.body._id
) or many (via req.body.ids
).
The tags to be removed are in req.body.tags
.
# POST /modules/apostrophe-pages/trash
Implement the batch trash route, which can trash many pages (via req.body.ids) and responds with a job id.
# POST /modules/apostrophe-pages/rescue
Implement the batch rescue route, which can rescue many pages (via req.body.ids) and responds with a job id. Cannot be invoked when trashInSchema is false, as there is no sensible way to place them when they return to the tree - better to drag them out of the trash.