## Documentation: # Overview PocketPages is a 23KB pluggable Multi-Page Application (MPA) preprocessor for PocketBase. It ships with an EJS template engine and uses a flexible plugin system that lets you efficiently choose additional features (including other templating engines) for your application while maintaining maximum performance. Use PocketPages to create slim and modern web apps: - Start small and iterate - SEO-first principals - No build step - Fast server-side rendering ## Hello world in just one file `// index.ejs <%= `Hello, world!` %> ` http://localhost:8090 Hello, world! With PocketPages, building apps is easy again. Inside `<%` and `%>`, you can leverage the full power of ES6 JavaScript inside PocketBase's [JSVM](https://pocketbase.io/jsvm/index.html), which exposes all of PocketBase's built-in functions. ## Retro file-based routing PocketPages borrows inspiration from SvelteKit's [file-based routing](https://kit.svelte.dev/docs/routing) architecture, so you can do this: ``` // +layout.ejs
<%- slot> ``` http://localhost:8090 Hello, world! ## Documentation: /overview # Overview PocketPages is a 23KB pluggable Multi-Page Application (MPA) preprocessor for PocketBase. It ships with an EJS template engine and uses a flexible plugin system that lets you efficiently choose additional features (including other templating engines) for your application while maintaining maximum performance. Use PocketPages to create slim and modern web apps: - Start small and iterate - SEO-first principals - No build step - Fast server-side rendering ## Hello world in just one file `// index.ejs <%= `Hello, world!` %> ` http://localhost:8090 Hello, world! With PocketPages, building apps is easy again. Inside `<%` and `%>`, you can leverage the full power of ES6 JavaScript inside PocketBase's [JSVM](https://pocketbase.io/jsvm/index.html), which exposes all of PocketBase's built-in functions. ## Retro file-based routing PocketPages borrows inspiration from SvelteKit's [file-based routing](https://kit.svelte.dev/docs/routing) architecture, so you can do this: ``` // +layout.ejs <%- slot> ``` http://localhost:8090 Hello, world! ## Documentation: /installation # Installation PocketPages requires PocketBase v0.25.5 or later. Installing PocketPages is easy. Step 1: Copy the minimal starter. `npx tiged benallfree/pocketpages/packages/starters/minimal . cd minimal npm install` Step 2: Run. ``` pocketbase --dir=minimal/pb_data --dev serve ``` Note: `--dir=pb_data` is necessary to tell PocketBase to use the current directory for data storage. Browse to `http://localhost:8090` and with any luck at all, you'll see: http://localhost:8090 Hello, world! To start editing, find `./pb_hooks/pages/index.ejs ` Changes appear immediately on the next refresh. ## How does it work? PocketPages is a Multi-Page Application (MPA) preprocessor for PocketBase. It listens for requests and renders pages using file-based routing rules like old school PHP. ## Documentation: /starter-kits # Starter Kits - [minimal](#minimal) - [daisyui](#daisyui) - [deploy-pockethost-manual](#deploy-pockethost-manual) - [deploy-pockethost-ga](#deploy-pockethost-ga) - [deploy-fly-manual](#deploy-fly-manual) - [deploy-fly-ga](#deploy-fly-ga) - [htmx](#htmx) - [vscode/cursor](#vscodecursor) - [mvp](#mvp) - [auth](#auth) The easiest way to get started with PocketPages is to use one of the starter kits. ## minimal The Minimal starter kit creates the absolute most minimal PocketPages app: a single `index.ejs` home page. `npx tiged benallfree/pocketpages/starters/minimal . cd minimal npm i pocketbase --dir=pb_data --dev serve ` Browse to `http://localhost:8090` and with any luck at all, you'll see: http://localhost:8090 Hello, world! To start editing, find `./pb_hooks/pages/index.ejs ` Changes appear immediately on the next refresh. For more detail, see [https://github.com/benallfree/pocketpages/blob/master/starters/minimal](https://github.com/benallfree/pocketpages/blob/master/starters/minimal) ## daisyui The `daisyui` starter kit incorporates Daisy UI and Tailwind. `npx tiged benallfree/pocketpages/starters/daisyui . cd daisyui npm i pocketbase --dir=pb_data --dev serve ` For more detail, see [https://github.com/benallfree/pocketpages/blob/master/starters/daisyui](https://github.com/benallfree/pocketpages/blob/master/starters/daisyui) ## deploy-pockethost-manual This kit helps you deploy manually to pockethost.io. See the [deployment guide](/docs/deploying) for more details. ## deploy-pockethost-ga This kit helps you deploy as a Github Action to pockethost.io. See the [deployment guide](/docs/deploying) for more details. ## deploy-fly-manual This kit helps you deploy manually to fly.io. See the [deployment guide](/docs/deploying) for more details. ## deploy-fly-ga This kit helps you deploy via Github Action to fly.io. It requires `deploy-fly-manual` as well. See the [deployment guide](/docs/deploying) for more details. ## htmx This kit helps you get started with the [htmx](https://htmx.org/) project. See the [starter kit README](https://github.com/benallfree/pocketpages/blob/master/starters/htmx/README.md) for more details. `npx tiged benallfree/pocketpages/starters/htmx . cd htmx npm i pocketbase --dir=pb_data --dev serve ` ## vscode/cursor This kit provides a VSCode/Cursor configuration for PocketPages. See the [starter kit README](https://github.com/benallfree/pocketpages/blob/master/starters/vscode/README.md) for more details. ## mvp This kit provides a starter kit based on the [MVP.css](https://andybrewer.github.io/mvp/) project. MVP.css is a minimal styling solution for building fast, modern web apps. It takes all the guesswork out of styling your project, leaving you to focus on features and functionality. `npx tiged benallfree/pocketpages/starters/mvp . cd mvp npm i pocketbase --dir=pb_data --dev serve ` For more detail, see the [MVP starter kit README](https://github.com/benallfree/pocketpages/blob/master/starters/mvp/README.md). ## auth This kit demonstrates Multi Page App (MPA) authentication using PocketPages. It includes: - User registration and login flows - Password reset functionality - Email verification - Email change confirmation - Local mail testing setup with MailDev `npx tiged benallfree/pocketpages/starters/auth . cd auth npm i pocketbase --dir=pb_data --dev serve ` For more detail, see the [auth starter kit README](https://github.com/benallfree/pocketpages/blob/master/starters/auth/README.md). ## Documentation: /upgrading # Upgrading PocketPages PocketPages ships as an NPM package. Keeping it up to date ensures you have access to the latest features, improvements, and security patches. The upgrade process is simple: ``` # Update PocketPages in your project npm i pocketpages@latest ``` ## Documentation: /creating-a-page # Creating a PocketPages Page PocketPages allows you to create dynamic pages for your application using EJS templating. Below are the steps and guidelines to create a page that will be served by PocketPages. ## Creating Pages - Create an EJS File: Inside the `pb_hooks/pages/` directory, create an `.ejs` file. This file will represent the page that you want to serve. For example, to create an "About" page: `pb_hooks/pages/about.ejs ` - Special Case - `index.ejs`: The `index.ejs` file has a special role at any directory level. At the root level: `pb_hooks/pages/index.ejs -> serves "/" ` In subdirectories: `pb_hooks/pages/products/index.ejs -> serves "/products" ` - Serving Pages: Any `.ejs` file within `pb_hooks/pages/` can be served directly. The name of the file (minus the `.ejs` extension) will correspond to the URL path. Example file structure: `pb_hooks/pages/ ├── index.ejs -> "/" ├── about.ejs -> "/about" ├── contact.ejs -> "/contact" └── products/ ├── index.ejs -> "/products" └── details.ejs -> "/products/details" ` - Nested Pages: You can organize your pages into subdirectories. The routing will follow the directory structure. If no specific file matches the route, an `index.ejs` in the corresponding directory will be served. Example routing table: ``` URL Path File Path / pb_hooks/pages/index.ejs /about pb_hooks/pages/about.ejs /contact pb_hooks/pages/contact.ejs /products pb_hooks/pages/products/index.ejs /products/details pb_hooks/pages/products/details.ejs ``` ## Documentation: /directory-structure # Directory Structure All PocketPages must reside within the `pb_hooks/pages/` directory of your project. This is the designated location where PocketPages looks for pages to serve. ### Example Structure: ``` pb_hooks/ pages/ index.ejs about.ejs contact.ejs products/ index.ejs details.ejs ``` ## Documentation: /routing # File-Based Routing in PocketPages PocketPages supports file-based routing, allowing you to create a clean and intuitive URL structure directly from your file and folder organization. This guide will discuss how to set up file-based routing using a nested directory structure, ensuring your application's URLs align with the layout of your EJS files. ## Understanding File-Based Routing File-based routing means that the URLs in your application are automatically determined by the file and folder structure within the `pb_hooks/pages/` directory. Each `.ejs` file corresponds to a unique route, and the nested folders reflect URL paths. ### Special Files and Directories PocketPages has two special naming conventions that affect routing: - Directories named `_private` are never routable and are used for storing private files - Files that begin with `+` (like `+load.js` and `+layout.ejs`) are special PocketPages files and are not routable For example: `pb_hooks/ pages/ _private/ # Not routable - for private files header.ejs config.js products/ +load.js # Not routable - special PocketPages file +layout.ejs # Not routable - special PocketPages file index.ejs # Routable at /products details.ejs # Routable at /products/details ` ### Basic Example Consider the following directory structure: `pb_hooks/ pages/ about.ejs contact.ejs index.ejs products/ details.ejs index.ejs reviews/ index.ejs latest.ejs ` ### How the Routing Works - Root-Level Routing: `/ -> pb_hooks/pages/index.ejs /about -> pb_hooks/pages/about.ejs /contact -> pb_hooks/pages/contact.ejs ` - Nested Routing: `/products -> pb_hooks/pages/products/index.ejs /products/details -> pb_hooks/pages/products/details.ejs ` - Deeper Nesting: `/products/reviews -> pb_hooks/pages/products/reviews/index.ejs /products/reviews/latest -> pb_hooks/pages/products/reviews/latest.ejs ` ## Organizing API Routes and Layouts When building applications that serve both full pages and API endpoints (especially with HTMX), it's important to properly organize your routes to prevent unwanted layout inheritance. ### Recommended Structure `pb_hooks/ pages/ (site)/ # Pages that should inherit layouts +layout.ejs # Main site layout about.ejs products/ index.ejs xapi/ # API endpoints (no layouts) count.ejs users.ejs +layout.ejs # Global layout (if needed) ` ### Example For an HTMX application: `
Current count: <%= count %> ` ### Practical Example Consider the following files in your `pb_hooks/pages/` directory: `pb_hooks/ pages/ foo/ index.ejs bar.ejs image.png ` When you visit `/foo`, it will redirect to `/foo/`. As a result: `/foo/ -> serves index.ejs /foo/image.png -> serves image.png /foo/bar -> serves bar.ejs ` This behavior ensures that your URLs remain clean and logical while maintaining the ability to use relative paths within your EJS templates efficiently. ## Documentation: /deploying # Deploying to Production PocketPages is easy to deploy. If you follow the [recommended project structure](/docs/structure), everything in `/pb_*` can be deployed. The two most popular ways to go live with PocketBase are to use [pockethost.io](https://pockethost.io) or to self-host using [Fly.io](https://fly.io). ## Recommended: Deploy to pockethost.io pockethost.io is the premiere PocketBase hosting service trusted by over 10,000 developers and millions of end users. In under 30 seconds, you can provision a free instance with unlimited (Fair Use) resources. ### Manual Deployment The easiest way to deploy is using the PocketHost.IO CLI utility (PHIO). Here's how: - Install the PHIO CLI globally: `npm i -g phio ` - Login to your PocketHost account: ``` phio login ``` - Link your local project to your PocketHost instance: ``` phio link ``` - Deploy your project: ``` phio deploy ``` ### Github Actions Deployment To set up automated deployments using Github Actions: `cp -r node_modules/pocketpages/starters/deploy-pockethost-ga . ` You'll need to set a few Github secrets. Look in the YAML file for details. ## Deploy to Fly.io Warning: Self-hosting is an advanced setup. I know Fly pretty well and it still took me an hour. To set up Fly.io deployment: `cp -r node_modules/pocketpages/starters/deploy-fly-ga . ` After this, you should see a `Dockerfile` and `fly.toml`. Use `fly launch` and `fly deploy` to create a Fly app and deploy it. For more information, see [Host for Free on Fly.io](https://github.com/pocketbase/pocketbase/discussions/537) ## Documentation: /parameters # Route and Query Parameters PocketPages lets you access both route parameters (from URL paths) and query parameters (from query strings) in your templates. ## Route Parameters Use square brackets `[]` in file or directory names to capture URL path segments as parameters. ### Example Structure `pb_hooks/pages/ products/ [productId]/ index.ejs [productId]/reviews/ [reviewId].ejs ` This structure handles URLs like: - `/products/123` - `/products/123/reviews/456` ### Using Route Parameters Access route parameters through `params` in your templates: `
Product: <%= params.productId %>
Product: <%= params.productId %>
Review: <%= params.reviewId %>
` ## Query Parameters Query parameters come from the URL's query string (after the `?`). They're also available through `params`: `/products/123?sort=latest&highlight=true ` Access them the same way in templates: `
Sort by: <%= params.sort %>
Highlight: <%= params.highlight %>
` ## Parameter Priority Query parameters override route parameters when they have the same name: `/products/123?productId=789 ` Here, `params.productId` will be `789`, not `123`. ## Complete Example URL: `/products/123/reviews/456?sort=latest&highlight=true ` Template: ```
Product: <%= params.productId %>
Review: <%= params.reviewId %>
Sort by: <%= params.sort %>
Highlight: <%= params.highlight %>
``` ## Documentation: /loading-data # Loading Data in PocketPages There are several ways to load data in PocketPages, from simple inline loading to more structured approaches. ## Inline Data Loading The simplest way to load data is directly in your template using `<% %>` tags at the top of your file: `<% const products = findRecordsByFilter('products', { filter: 'active = true', sort: 'name' }) %> Products
Products
<% products.forEach(product => { %>
<%= product.name %>
<% }) %> ` ## Server Script Blocks For better code formatting support in editors like VSCode, you can use the ` Products
<% }) %> ` ## Structured Data Loading with +load.js For more complex data loading needs, PocketPages provides `+load.js` files. These are useful when you need to: - Share data loading logic across multiple templates - Separate concerns between data loading and presentation - Handle complex routing scenarios - Implement method-specific loading (GET, POST, etc.) ### Basic Usage `/** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { const { findRecordsByFilter } = api const products = findRecordsByFilter('products', { filter: 'active = true', sort: 'name', }) return { products } } ` ## File Structure Data loaders follow your route hierarchy: `pb_hooks/pages/ +load.js # Only executes if index.ejs is the entry point index.ejs # Home page entry point products/ +load.js # Only executes if products/index.ejs is the entry point [id]/ +load.js # Only executes if products/[id]/index.ejs is the entry point +get.js # Only executes for GET requests if products/[id]/index.ejs is the entry point index.ejs # Template using data ` Important: Only a single `+load.js` file executes per request - the one at the same level as the entry point EJS file. This is different from `+middleware.js` files, which execute hierarchically from root to leaf along the route path. Use middleware when you need cascading data loading. ### Example: Route `/products/123` If the route resolves to `/products/123/index.ejs`: - Loader Execution (single file): `/products/[id]/+load.js # Only this loader executes ` - Method-Specific Loader Execution (single file): `/products/[id]/+get.js # Only this method-specific loader executes ` - Template Rendering: `/products/[id]/index.ejs ` ## Example Scenarios ### Product List (`/products`) ``` /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { const { findRecordsByFilter } = api const products = findRecordsByFilter('products', { filter: 'active = true', sort: '-created', }) return { products } } ``` ### Product Details (`/products/[id]`) ``` /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { const { findRecordByFilter, params, response } = api const product = findRecordByFilter('products', { filter: `id = "${params.id}"`, }) if (!product) { response.status(404) return { error: 'Product not found' } } return { product } } ``` ## HTTP Method-Specific Loaders In addition to `+load.js`, PocketPages supports method-specific loaders that only execute for their respective HTTP methods: `pb_hooks/pages/ contact/ +load.js # Runs for all methods +get.js # Runs only for GET, after +load.js +post.js # Runs only for POST, after +load.js index.ejs ` ### Execution Order - `+load.js` executes first (if present) - Method-specific loader executes second (if present) - Data from both loaders is merged ### Example: Contact Form ``` // contact/+load.js - Runs for all methods /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { return { departments: findRecordsByFilter('departments', { filter: 'active = true', }), } } // contact/+get.js - Runs only for GET requests /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { return { csrf: generateToken(), } } // contact/+post.js - Runs only for POST requests /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function (api) { const { formData, redirect } = api try { const message = findRecordByFilter('messages', { create: { email: formData.email, message: formData.message, department: formData.department, }, }) redirect('/contact/success') } catch (error) { return { error: 'Failed to send message', values: formData, } } } ``` ### Available Method Loaders - `+load.js` - Runs for all HTTP methods - `+get.js` - GET requests only - `+post.js` - POST requests only - `+put.js` - PUT requests only - `+patch.js` - PATCH requests only - `+delete.js` - DELETE requests only Note: Method-specific loaders are optional. If not present, only `+load.js` (if it exists) will execute. ## Using Data in Templates ### Basic Example ```
<%= data.siteName %>
``` ### Complete Example ``` <%= data.product?.name || 'Products' %> | <%= data.siteName %> <% if (data.error) { %>
<% } %> ``` ## Reference - [API Documentation](/docs/api) - [Middleware Guide](/docs/middleware) - [TypeScript Types](/docs/types) ## Documentation: /json # Serving JSON Responses with PocketPages PocketPages allows you to easily create RESTful APIs by returning JSON content from your `.ejs` templates. If the output of an `.ejs` template parses as valid JSON, PocketPages will automatically serve it as a JSON response. This feature simplifies the process of building APIs within your PocketPages application, enabling you to handle both HTML and JSON responses using the same templating system. ## How It Works When an `.ejs` template returns content that can be parsed as valid JSON, PocketPages detects this and serves the response with the appropriate `Content-Type: application/json` header. There are several ways to return JSON from your templates: ### 1. Return an Object Directly The simplest way is to return an object directly from your template: `<% return { status: "success", data: { productId: params.productId, name: "Example Product", price: 29.99 } } %> ` ### 2. Using `echo()` You can use the `echo()` function with an object: `<% const response = { status: "success", data: { productId: params.productId, name: "Example Product", price: 29.99 } } echo(response) %> ` ### 3. Using JSON.stringify You can also explicitly stringify your JSON: `<% const response = { status: "success", data: { productId: params.productId, name: "Example Product", price: 29.99 } } %> <%= JSON.stringify(response) %> ` ### Example URL and Response Given the following template structure: `pb_hooks/ pages/ api/ product/ [productId].ejs ` A request to `/api/product/123` might return the following JSON response: ``` { "status": "success", "data": { "productId": "123", "name": "Example Product", "price": 29.99 } } ``` ### Benefits of Using `.ejs` for JSON Responses - Unified Templating: Use the same `.ejs` templating system for both HTML and JSON responses, reducing the need to learn multiple technologies. - Dynamic Content: Easily incorporate dynamic data from route parameters, query strings, or database queries into your JSON responses. - Simplified API Development: Quickly build RESTful APIs alongside your web pages without needing a separate framework or toolset. ### Best Practices - Ensure Valid JSON: Always make sure that the content returned by your `.ejs` templates is valid JSON. If the content cannot be parsed as JSON, PocketPages will not serve it as a JSON response. - Use for APIs: Leverage this feature to build RESTful APIs where the same route structure can return either HTML or JSON, depending on the request and the content generated. - Separate Concerns: For clarity and maintainability, consider organizing your API-related templates in a dedicated folder structure, such as `pages/api/`, to distinguish them from your regular HTML templates. ## Documentation: /partials # Using Partials in PocketPages Partials are reusable templates that you can include within other files. PocketPages implements an innovative partial resolution system that makes organizing and using partials both secure and convenient. ## The _private Directory System PocketPages looks for included files in `_private` directories, starting from the including template's directory and working up through parent directories. This system has several benefits: - Directories starting with underscore (like `_private`) are never routed, ensuring included files can't be accessed directly - Partials can be "hoisted" to common ancestor directories to be shared by multiple components - Each directory can have its own private components that are only relevant to that section - Child directories can override parent partials by having their own `_private` file with the same name - Path management is greatly simplified since you rarely need to specify relative paths - just the partial name is usually enough ### Example Directory Structure `pb_hooks/ pages/ _private/ global-header.ejs global-footer.ejs docs.md # Markdown file processed after EJS blog/ _private/ blog-sidebar.ejs posts/ _private/ post-card.ejs index.ejs index.ejs index.ejs ` ## Including Partials To include a partial, use the standard EJS include syntax: `<%- include('header.ejs', { title: 'My Page' }) %> ` The included file will be processed through: - EJS template engine first - Any configured plugins that handle that file type (e.g., markdown files will be processed by the markdown plugin) For example: ` <%- include('docs.md', { section: 'overview' }) %> <%- include('header.ejs', { title: 'My Page' }) %> ` ### Partial Resolution When resolving the path to a partial, PocketPages will: - Start in the current template's directory, looking for `_private/header.ejs` - If not found, check the parent directory for `_private/header.ejs` - Continue up the directory tree until the file is found or the root is reached You have several options for controlling this resolution: - Simple name: Just specify the file name (e.g., `header.ejs`) to use the automatic resolution system - Absolute paths: Start with `/` to specify an explicit path from the root (e.g., `/docs/_private/header.ejs`) - Level jumping: Use `../` prefix to skip the local `_private` directory and force resolution from a parent level (useful when you want to access an ancestor's partial that has the same name as a local one) ### Examples Given the directory structure above: `// In /blog/posts/index.ejs: <%- include('post-card.ejs', { api }) %> // Uses /blog/posts/_private/post-card.ejs <%- include('blog-sidebar.ejs', { api }) %> // Uses /blog/_private/blog-sidebar.ejs <%- include('global-header.ejs', { api }) %> // Uses /_private/global-header.ejs // Using absolute path <%- include('/blog/_private/blog-sidebar.ejs', { api }) %> // Explicitly uses /blog/_private/blog-sidebar.ejs // Level jumping <%- include('../layout.ejs', { api }) %> // Skips local _private/layout.ejs and uses parent's version ` ### Passing Data to Partials When including partials, it's recommended to pass only the specific data they need to render: `<%- include('header.ejs', { title: 'My Page', user: currentUser }) %> ` Inside the partial, access the passed data directly: `
<%= title %>
<% if (user) { %> <% } %> ` #### Avoiding Side Effects While it's possible to pass the PocketPages `api` object to partials, this is considered an antipattern because: - It makes partials less predictable by allowing them to perform side effects - It creates hidden dependencies between partials and the PocketPages system - It makes testing and maintaining partials more difficult - It breaks the principle of separation of concerns Instead, compute any necessary data in your main template or `+load.js` file, and pass only the required values to your partials. This keeps partials pure and deterministic: ` <%- include('user-card.ejs', { username: user.name, avatar: user.avatarUrl, joinDate: formatDate(user.joinedAt) }) %> <%- include('user-card.ejs', { api }) %> // Don't do this ` ## Best Practices - Hoist Common Partials: If multiple sections need the same partial, move it up to a common ancestor's `_private` directory. - Local Partials: Keep section-specific partials in that section's `_private` directory. - Avoid Deep Nesting: While technically possible to reference sibling directories' `_private` folders (e.g., `../sibling/_private/file.ejs`), it's generally cleaner to hoist shared components up instead. - Consistent Naming: Use clear, descriptive names for your partials to make their purpose obvious. ## Example: Organizing Shared Components `pb_hooks/ pages/ _private/ // Global components used everywhere header.ejs footer.ejs docs.md // Markdown files processed after EJS docs/ _private/ // Documentation-specific components sidebar.ejs code-block.ejs getting-started/ _private/ // Components specific to getting started tutorial-nav.ejs index.ejs index.ejs index.ejs ` This structure makes it easy to: - Share global components across all pages - Keep documentation-specific components together - Isolate section-specific components - Maintain clear boundaries between different parts of your site ## Documentation: /layouts # Working with PocketPages Layouts PocketPages provides a powerful layout system that allows you to create consistent page structures by defining layouts that can wrap your content. Layouts are defined using `+layout.ejs` files and are applied in a bottom-up order, meaning that each higher-level layout wraps the content of the child layout. This guide will explain how layouts work in PocketPages and how to use them effectively. ## What is a Layout? A layout in PocketPages is an EJS template file named `+layout.ejs`. Layouts are used to define the overall structure of your pages, such as headers, footers, and common navigation elements, which can be shared across multiple pages. Layouts are traversed in a bottom-up order, meaning that the layout closest to the content is applied first, and each parent layout wraps the child layout. ### Example Directory Structure `pb_hooks/pages/ +layout.ejs products/ +layout.ejs [productId]/ index.ejs details.ejs ` ### How Layouts are Applied - Leaf Page: The `index.ejs` or `details.ejs` file is the leaf page, where the final content is rendered. - Child Layout: The `+layout.ejs` in the `[productId]/` directory is applied first, wrapping the content of `index.ejs` or `details.ejs`. - Parent Layout: The `+layout.ejs` in the `products/` directory is then applied, wrapping the content from the child layout. - Root Layout: Finally, the `+layout.ejs` at the root level (`pages/+layout.ejs`) wraps the entire content, providing the final structure of the page. ### Using the `slot` Variable In `+layout.ejs` files, a special `slot` variable is provided in the context. This variable represents the content that will be placed inside the layout. The `slot` variable is available only in `+layout.ejs` files. #### Example Usage of `slot` ` My Application
Welcome to My Application
<%- slot %> ` In this example, the `slot` variable is used to place the interior content (from the child layout or the leaf page) within the `` tag of the layout. ### Multiple Slots PocketPages provides two ways to work with content in layouts: the `slot` string which always contains the full content, and a `slots` object for accessing named sections of that content. #### Basic Slot Usage For simple layouts with a single content area, you can use the `slot` variable which contains all the content: ` <%- slot %> ` #### Working with Multiple Slots For more complex layouts, you can divide your content into named sections using HTML comments and access them through the `slots` object, while `slot` continues to contain the complete content: `
Product Details
This is the main content of the product page.
` Then in your layout, access these named slots using the `slots` object: ` Product Page <%- slots.header || 'Default Header' %>
<%- slots.sidebar %>
<%- slots.body || slot %>
<%- slot %>
` Note the following patterns: - The `slot` variable always contains the complete content, whether or not slots are defined - Use `slots.name` to access a specific named section - Provide defaults with `slots.name || 'Default Content'` - Use `slots.body || slot` to create a main content area that shows either the body slot or all content ### Leaf `+load.js` Data Availability In layouts, the `data` object from the `+load.js` file at the leaf level (e.g., `index.ejs`) is available. However, it is important to note that the `data` object from the `+load.js` file at the layout level is not available in the layout itself. This means that the layout can access only the data passed from the leaf page. ### When Layouts are Not Applied Layouts are only applied to leaf EJS files that return HTML content. If an EJS file returns a JSON response, the layouts will not be executed. This ensures that layouts are used only for rendering HTML pages, keeping API responses clean and efficient. ## Documentation: /structure # Recommended Project Structure This recommended project structure makes it easy to upload everything in `/pb_*` directly to your production PocketBase instance. It also mirrors the directory structure of [pockethost.io](https://pockethost.io) and PocketBase's sample directory names. ``` package.json pb_hooks/ pages/ +boot.pb.js index.ejs pb_migrations/ pb_data/ storage/ data.db logs.db ``` ## Documentation: /private-files # Working with Private Files and Directories ## File Naming Conventions PocketPages has two special naming conventions: - Directories named `_private` are used for private files and are never publicly routable - Files that begin with `+` (like `+load.js` and `+layout.ejs`) are special PocketPages files and are not routable Examples: `pb_hooks/ pages/ _private/ # Private directory (not routable) helpers/ # Normal directory (routable) +load.js # Special PocketPages file (not routable) +layout.ejs # Special PocketPages file (not routable) index.ejs # Normal route file (routable) ` PocketPages implements a powerful system for managing private files through `_private` directories. This system serves two main purposes: - Organizing and including template partials via `include()` - Loading JavaScript modules via `resolve()` Both functions follow the same directory traversal pattern, making it intuitive to organize and access private files throughout your application. ## The `_private` Directory System ### Core Concepts - Directories named `_private` are never publicly routable - Files are resolved by searching `_private` directories up the directory tree - Each section can have its own private files - Child directories can override parent files of the same name - Files can be "hoisted" to ancestor directories to be shared ### Example Directory Structure `pb_hooks/ pages/ _private/ # Global shared files layout.ejs # Base layout template auth.js # Authentication utilities config.js # Global configuration products/ _private/ # Product-specific files product-card.ejs # Product display partial queries.js # Product database queries categories/ _private/ # Category-specific files category-nav.ejs # Category navigation partial helpers.js # Category-specific utilities index.ejs index.ejs index.ejs ` ## File Resolution When you call `include()` or `resolve()`, PocketPages: - Starts in the current template's directory - Looks for the file in that directory's `_private` folder - If not found, moves up to the parent directory and checks its `_private` folder - Continues until the file is found or reaches the root This creates a natural hierarchy where: - Files used by many pages live higher in the tree - Files specific to a section stay close to where they're used - Override files when needed by creating local versions ### Example Resolution `<% // In /products/categories/index.ejs: // Looks for category-nav first in local _private, then up the tree include('category-nav.ejs') // Uses /products/categories/_private/category-nav.ejs // Product card isn't in local _private, so checks parent directories include('product-card.ejs') // Uses /products/_private/product-card.ejs // Global layout is found in the root _private include('layout.ejs') // Uses /_private/layout.ejs // Same pattern works for resolve const helpers = resolve('helpers') // Uses local /categories/_private/helpers.js const queries = resolve('queries') // Uses parent /products/_private/queries.js const config = resolve('config') // Uses root /_private/config.js %> ` ## Path Control While the automatic resolution system handles most cases elegantly, you sometimes need more control: `// Absolute paths start from the root include('/products/_private/product-card.ejs') resolve('/products/_private/queries') // Use ../ to skip the local _private and force parent resolution include('../layout.ejs') resolve('../helpers') ` ## Best Practices ### 1. Proximity Principle Keep private files close to where they're used: `products/ _private/ product-card.ejs # Used only in products section product-queries.js # Product-specific database logic index.ejs ` ### 2. Strategic Hoisting Move files up the tree when they become widely used: `_private/ layout.ejs # Used across the site auth.js # Global authentication products/ _private/ product-card.ejs # Used only in products ` ### 3. Logical Overrides Override parent files when needed for specialization: `_private/ header.ejs # Default site header admin/ _private/ header.ejs # Special admin header ` ### 4. Clean Interfaces Export clear, focused interfaces from JavaScript modules: `// _private/database.js module.exports = { query: (sql, params) => { // Database query logic }, getRecord: (id) => { // Record retrieval logic }, } ` ## Important Notes - Files in `_private` directories are never publicly accessible - Changes to private files require a server restart in production - Use environment variables for secrets, not private files - Private files are perfect for: Reusable template components - Server-side utilities - Configuration - Database queries - Business logic ## The Power of Convention This system's strength comes from its conventions: - Predictable file location - Natural code organization - Clear separation of concerns - Easy code sharing - Simple overrides - Minimal path management By following these patterns, you can build maintainable applications where code lives where it makes sense, while still being easy to find and use. See [resolve](/docs/api/resolve) and [Using Partials](/docs/partials) for detailed documentation of the specific functions. ## Documentation: /middleware # Middleware Middleware files (`+middleware.js`) process requests before they reach your templates. Use middleware to: - Check authentication - Load shared data - Set headers - Validate requests - Handle errors ## Basic Example `/** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { const { request, redirect } = api // Check auth if (!request.header('Authorization')) { redirect('/login') return {} } // Load data const categories = findRecordsByFilter('categories', { filter: 'active = true', }) return { categories } } ` ## How Middleware Works Middleware executes hierarchically from root to leaf: `pb_hooks/pages/ +middleware.js # Runs first (all routes) products/ +middleware.js # Runs second (/products/*) [id]/ +middleware.js # Runs third (/products/[id]/*) ` For URL `/products/123`: - `/+middleware.js` runs - `/products/+middleware.js` runs - `/products/[id]/+middleware.js` runs ## Common Use Cases ### Auth Guard `/** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { const { request, redirect } = api const session = request.cookie('session') if (!session) { redirect('/login') return {} } const user = findRecordByFilter('users', { filter: `session = "${session}"`, }) return { user } } ` ### Request Validation ``` /** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { const { request, response, params } = api if (!params.id?.match(/^[0-9]+$/)) { response.status(400) return { error: 'Invalid ID' } } return {} } ``` ### Loading Data ``` /** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { const { params, response } = api const product = findRecordByFilter('products', { filter: `id = "${params.id}"`, }) if (!product) { response.status(404) return { error: 'Not found' } } return { product } } ``` ## Using Middleware Data Access middleware data in templates through `data`: `<% if (data.user) { %>
Welcome, <%= data.user.name %>
<% if (data.product) { %>
<%= data.product.name %>
<%= data.product.description %>
<% } %> <% } %> ` ## Important Notes - Middleware runs before page loaders (`+load.js`) - All middleware along the route path executes - Return an object to pass data to templates - Return early to stop processing - Keep processing efficient ## HTTP Method-Specific Middleware In addition to `+middleware.js` which runs for all HTTP methods, you can create method-specific middleware files that only run for specific HTTP methods: `// +get.js - Only runs for GET requests /** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { // Handle GET requests return {} } // +post.js - Only runs for POST requests /** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { // Handle POST requests return {} } ` Supported method-specific middleware files: - `+get.js` - `+post.js` - `+put.js` - `+patch.js` - `+delete.js` These files follow the same hierarchical execution order as `+middleware.js`. ## Reference - [Loading Data Guide](/docs/loading-data) - [API Documentation](/docs/api) ## Documentation: /asset-management # Asset Management PocketPages provides built-in asset management with content-based fingerprinting for efficient cache invalidation. ## Directory Structure Place your static assets (images, CSS, JS, etc.) alongside the pages that use them: `pb_hooks/ pages/ feature/ index.md # Your content styles.css # Feature-specific styles header.jpg # Feature-specific image shared/ logo.png # Shared across pages ` ## Using Assets ### In Markdown Use standard markdown syntax for images - paths are automatically resolved relative to the current file: ` # Local image  # Absolute path ` ### In EJS Templates Use the `asset()` helper to generate fingerprinted URLs: ` ` ## How It Works - During startup, PocketPages generates content-based fingerprints for all static files - The `asset()` helper and markdown images automatically generate fingerprinted URLs - URLs are served with long-term cache headers for optimal performance ## Best Practices - Keep assets close to the pages that use them - Use a shared directory (like `shared/` or `assets/`) for common resources - Always verify assets exist in your pages directory For detailed information about the `asset()` helper and its features, see the [asset helper documentation](/docs/request-context/asset). ## Documentation: /static-content # Serving Static Content in PocketPages In PocketPages, serving static content is straightforward and integrated seamlessly into the framework. Any file that isn't an EJS template (`*.ejs`), doesn't begin with a `+`, and isn't in a `_private` directory is treated as a static file. These files are served directly by the underlying Echo framework, which handles content type inference, streaming, and other necessary details. ## What is Considered Static Content? Static content refers to files that are served directly to the client without server-side processing or dynamic generation. These files typically include: - HTML files (`*.html`) - CSS files (`*.css`) - JavaScript files (`*.js`) - Image files (`*.jpg`, `*.png`, etc.) - Font files (`*.woff`, `*.ttf`, etc.) - Other binary files or documents (e.g., PDFs, videos) For dynamic asset handling in your templates and pages, PocketPages provides the `asset()` function that helps generate correct URLs for your static assets. This function ensures your assets are properly referenced regardless of your application's base path configuration. For more details on managing and organizing your static assets effectively, see the [Asset Management Guide](/docs/guides/asset-management). ### Example Directory Structure ``` my-pocketpages-app/ ├── pages/ │ ├── assets/ # Static assets directory │ │ ├── css/ │ │ │ ├── main.css │ │ │ └── components.css │ │ ├── js/ │ │ │ ├── app.js │ │ │ └── utils.js │ │ ├── images/ │ │ │ ├── logo.png │ │ │ └── hero.jpg │ │ └── fonts/ │ │ ├── OpenSans.woff2 │ │ └── Roboto.woff2 │ ├── documents/ # Static documents │ │ ├── terms.pdf │ │ └── privacy.pdf │ ├── layout.ejs # Layout template (not served as static) │ ├── +middleware.js # Middleware file (not served as static) │ ├── index.ejs # Dynamic page template │ └── about.html # Static HTML page ``` ## Documentation: /secrets # Managing Secrets in PocketPages When building secure applications, managing secrets like API keys, database credentials, and other sensitive information is crucial. PocketPages provides a secure way to access environment variables through the global `env()` function. ## Using the `env()` Function The `env()` function is available globally in both templates and JavaScript files. It provides secure access to environment variables: `const apiKey = env('API_KEY') const dbPassword = env('DB_PASSWORD') ` ### In Templates ``` <% const apiKey = env('API_KEY') %> ``` ### In JavaScript Files ``` /** @type {import('pocketpages').PageDataLoaderFunc} */ module.exports = function () { const apiKey = env('API_KEY') const dbUrl = env('DATABASE_URL') // Use environment variables for configuration return { config: { apiEndpoint: env('API_ENDPOINT'), // ... }, } } ``` ## Best Practices - Never Expose Secrets in Templates ` ` - Check for Required Variables `const apiKey = env('API_KEY') if (!apiKey) { error('Missing required API_KEY environment variable') // Handle error appropriately } ` - Use Descriptive Names `// Good const stripeSecretKey = env('STRIPE_SECRET_KEY') const mailgunApiKey = env('MAILGUN_API_KEY') // Avoid const key = env('KEY') const secret = env('SECRET') ` - Document Required Variables `/** * Required environment variables: * - API_KEY: External service API key * - DB_PASSWORD: Database password * - SMTP_PASSWORD: Email service password */ ` ## Setting Environment Variables Environment variables can be set in various ways depending on your deployment environment: - Development Environment `# .env file API_KEY=your_api_key DB_PASSWORD=your_db_password ` - Production Environment `# Set directly in your hosting environment export API_KEY=your_api_key export DB_PASSWORD=your_db_password ` - Docker Environment ``` ENV API_KEY=your_api_key ENV DB_PASSWORD=your_db_password ``` ## Security Considerations - Never commit secrets to version control - Use different values for development and production - Rotate secrets regularly - Use environment-specific configurations - Implement proper access controls ## Additional Resources - [Environment Variables Documentation](/docs/global-api/env) - [Deployment Guide](/docs/deploying) - [Security Best Practices](/docs/security) ## Documentation: /config # Custom Configuration with `+config.js` In the `pb_hooks/pages/` root, you can create a `+config.js` file to define custom configuration options for your application. This file allows you to control how files are processed within your app through plugins and other settings. `pb_hooks/ pages/ +config.js ` ## Example Configuration Here is an example of a basic `+config.js` file: `module.exports = { plugins: [ // String format (shorthand) 'pocketpages-plugin-ejs', // Object format with name { name: 'pocketpages-plugin-ejs', // npm package name extensions: ['.ejs', '.md'], debug: false, }, // Direct factory function (config, options) => ({ // plugin implementation }), // Object format with explicit factory function { fn: (config, options) => ({ // plugin implementation }), debug: false, // other options... }, ], debug: false, } ` ## Configuration Options - `plugins`: An array that specifies which plugins to use and their configurations. Plugins can be specified in several ways: As a string (shorthand for npm package name) - As an object with `name` property specifying an npm package - As a plugin factory function directly - As an object with `fn` property containing a factory function The `debug` option defaults to `false` for all plugin formats. - `debug`: A boolean that enables internal PocketPages debugging output to the console. When set to `true`, it will output information about routes, parameters, and other internal details that are helpful for troubleshooting. Defaults to `false`. ## Plugin Configuration Formats PocketPages supports several formats for configuring plugins. Here are all the valid formats: ## Plugin Processing Order The order of plugins in the `plugins` array determines their processing order. Each plugin's `onRender` hook is called in sequence, with the output of one plugin becoming the input for the next. For example, when processing a Markdown file with both EJS and Markdown plugins: `module.exports = { plugins: [ // Processes first - enables EJS in Markdown { name: 'pocketpages-plugin-ejs', extensions: ['.ejs', '.md'], }, // Processes second - converts Markdown to HTML 'pocketpages-plugin-marked', ], } ` This configuration allows you to: - Use EJS features within Markdown files - Process the resulting content as Markdown - Output the final HTML Changing the order would process Markdown first, then EJS - which might be desirable in some cases but would prevent using EJS features within Markdown files. `module.exports = { plugins: [ // Format 1: String shorthand (npm package name) 'pocketpages-plugin-ejs', // Format 2: Object with npm package name { name: 'pocketpages-plugin-ejs', debug: false, // Any other options are passed to the plugin extensions: ['.ejs', '.md'], }, // Format 3: Direct factory function (config, options) => ({ onRequest(context) { config.global.dbg('Request:', context.request.method) }, }), // Format 4: Object with explicit factory { fn: (config, options) => ({ onRequest(context) { config.global.dbg('Request:', context.request.method) }, }), debug: false, // Any other options are passed to the plugin }, ], } ` Important notes: - The `name` property should be the name of an npm package that exports a plugin factory - PocketPages plugins typically follow the naming convention `pocketpages-plugin-*` (e.g. `pocketpages-plugin-ejs`, `pocketpages-plugin-marked`) - If both `fn` and `name` are present in an object configuration, `fn` takes precedence - The `fn` property is the only reserved key in plugin configuration objects - All other properties in plugin configuration objects are passed as options to the plugin - The `debug` option defaults to `false` for all plugin formats ## Debug Options PocketPages provides two levels of debug output control: - Global debugging - Set via the top-level `debug` option. When true, enables debug output for all of PocketPages. - Per-plugin debugging - Each plugin can have its own `debug` option. This allows you to enable debug output for specific plugins while keeping others quiet. The plugin's debug option only affects debug calls within that plugin's scope. For example, to debug only the EJS plugin: ``` module.exports = { plugins: [ { name: 'pocketpages-plugin-ejs', debug: true, // Only EJS plugin will output debug info }, 'pocketpages-plugin-marked', // Debug defaults to false ], debug: false, // Global debugging remains off } ``` ## Documentation: /caching # Understanding Caching in PocketPages PocketPages uses content-based fingerprinting for static assets and development-mode cache busters to ensure efficient caching while maintaining quick updates during development. ## Asset Fingerprinting ### How It Works - During startup, PocketPages computes SHA-256 fingerprints for all static assets - The `asset()` helper embeds these fingerprints into filenames - The router dynamically matches fingerprinted URLs to their actual files ` ` ### File Resolution PocketPages resolves asset paths relative to the current template: `pb_hooks/pages/ feature/ index.ejs # asset('image.png') → /feature/image.[hash].png image.png products/ details.ejs # asset('style.css') → /products/style.[hash].css style.css ` ## Development vs Production ### Development Mode When an asset doesn't exist in development: ` ` ### Production Mode When an asset doesn't exist in production: ` ` ## Best Practices - Keep Assets in Pages Directory `pb_hooks/pages/ images/ # Global images get fingerprinted logo.png feature/ header.jpg # Local images get fingerprinted index.ejs ` - Use Relative Paths for Local Assets ` ` - Verify Assets Exist Missing assets won't get fingerprinted - Development mode will show cache busters - Production mode will serve un-fingerprinted paths ## CDN Integration ### Cloudflare Example Fingerprinted assets work well with CDNs: - First request: `/images/logo.abc123de.png` CDN misses, fetches from origin - Origin matches `abc123de` to `logo.png` - CDN caches response indefinitely - Content update: File changes, new hash `xyz789fg` - Template outputs `/images/logo.xyz789fg.png` - CDN misses, fetches new version - Old version naturally expires ### Cache Control Headers ``` /** @type {import('pocketpages').MiddlewareLoaderFunc} */ module.exports = function (api) { const { response } = api // Set cache headers for static assets response.header('Cache-Control', 'public, max-age=31536000') return {} } ``` ## Complete Example ``` ``` ## Important Notes - Fingerprinting requires files to exist in pages directory - Development mode helps identify missing assets - CDNs can cache fingerprinted assets indefinitely - Use middleware to set cache control headers - Relative paths for local assets, absolute for global ## Reference - [asset() Documentation](/docs/api/asset) - [Middleware Guide](/docs/middleware) - [Cloudflare Cache Documentation](https://developers.cloudflare.com/cache)