PocketPages

repository·main·Indexed 43 days ago

https://github.com/benallfree/pocketpages

A high-performance server-side rendering engine for PocketBase that uses EJS templates and JS hooks to enable SEO-friendly web applications with fast response times (<40ms). It includes a CLI tool (create-pocketpages) for scaffolding projects and a plugin ecosystem featuring authentication, Datastar integration for realtime DOM updates, and EJS template enhancements.

Tokens
86.9K
Snippets
318
Records
417
Agent score
54%

What's inside PocketPages

  1. What is PocketPages?

    main

    PocketPages is a 23KB pluggable Multi-Page Application (MPA) preprocessor designed specifically for PocketBase. It uses an EJS template engine by default and features a flexible plugin system that allows you to add features (such as different templating engines) while maintaining high performance.

    Key characteristics:

    • SEO-first: Optimized for search engine visibility.
    • No build step: Simplifies development by removing the need for complex build pipelines.
    • Fast SSR: Provides efficient server-side rendering.
    • Pluggable: Choose only the features your application needs.
  2. Core capabilities of PocketPages

    main

    PocketPages is a web framework built on PocketBase that utilizes file-based routing and server-side rendering (SSR). It is designed for high performance using Golang and Goja, providing a lightweight footprint with minimal dependencies.

    Key architectural pillars include:

    • PocketBase Integration: Provides full access to the PocketBase JSVM API, native database integration, built-in authentication, and real-time capabilities.
    • Server-Side Rendering: Delivers SEO-friendly pages with fast initial loads and reduced client-side JavaScript, including support for meta tags and OpenGraph.
    • Routing: Uses an intuitive file-based routing system.
    • Templating: Uses EJS for dynamic content generation.
  3. What can PocketPages plugins do?

    main

    PocketPages uses a plugin architecture to extend functionality beyond simple templating. Plugins can hook into different stages of the application lifecycle to perform the following tasks:

    • Extend Request and Response: Use onRequest and onResponse hooks to modify how requests are processed and how responses are delivered.
    • Customize Rendering: Use the onRender hook to intercept and modify the final output.
    • Extend the API: Use the onExtendContextApi hook to add custom functions or integrate sub-applications into the API context.
  4. Understand plugin processing order

    main

    The order of plugins in the plugins array determines the sequence of the onRender hook execution. The output of one plugin becomes the input for the next plugin in the array.

    Example: Using EJS inside Markdown To use EJS features within Markdown files, you must place the EJS plugin before the Markdown plugin in the array. This ensures EJS processes the file first, and the resulting content is then passed to the Markdown plugin for HTML conversion.

    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',
      ],
    }
  5. How Smart Partial Resolution works

    main

    When using include(), the plugin performs a smart search for partials. It searches up the directory tree for _private directories. For a given path, it will:

    1. Look for an exact match of the specified path.
    2. If not found, try appending each configured extension.
    3. Repeat this process by moving up the directory tree into _private folders.

    Example: include('header') with extensions ['.ejs', '.md'] will search for header, header.ejs, and header.md within the current directory's _private folder, then the parent's _private folder, and so on, up to the root.

    <%%%- include('header', { title: 'My Page' }) %>
    
    <!-- You can also use explicit extensions -->
    <%%%- include('header.ejs', { title: 'My Page' }) %>
  6. How to structure a PocketPages plugin

    main

    A PocketPages plugin is implemented as a factory function that accepts two arguments: config (core system configuration) and options (plugin-specific settings). The function must return an object containing one or more hook methods to modify application behavior.

    Plugin Factory Parameters

    1. config: PluginFactoryConfig: Contains core system access:

      • pagesRoot: Root directory of pages.
      • config: Global PocketPages configuration.
      • global: The global context API.
      • routes: Array of route definitions.
      • dbg: A debug logging function.
    2. options: PluginOptionsBase: Contains plugin-specific settings, such as debug or custom configuration keys defined by the plugin author.

    module.exports = (config, options) => {
      // Initialize plugin with config and options
      return {
        // Hook methods
        onRequest(context) {
          /* ... */
        },
        onRender(context) {
          /* ... */
        },
        handles(context) {
          /* ... */
        },
        onExtendContextApi(context) {
          /* ... */
        },
        onResponse(context) {
          /* ... */
        },
      }
    }
  7. OAuth2 Security and Redirect Configuration

    main

    When using requestOAuth2Login, keep the following security and configuration requirements in mind:

    • CSRF Protection: The function automatically generates and stores a state parameter in a cookie to prevent Cross-Site Request Forgery attacks.
    • Callback Validation: The state and other OAuth2 parameters are stored in a cookie to be validated during the callback phase.
    • Redirect URL Matching: The final redirect URL is constructed using your application's base URL combined with the redirectPath option. Crucially, you must ensure this resulting URL matches exactly what you have configured in your OAuth2 provider's (e.g., Google, GitHub) developer settings.
  8. How SPA Mode works in Datastar

    main

    When SPA mode is enabled via the spa option in datastar.scripts(), the plugin provides a smooth single-page application experience:

    1. Link Interception: <a tags within the specified scope are modified to prevent default navigation.
    2. History Management: Clicking links updates browser history using pushState() without full page reloads.
    3. Content Updates: Target page content is fetched and injected into the DOM.
    4. Layout Handling: The plugin automatically sends the Datastar-Request: true header, which tells PocketPages to disable layout rendering (+layout.* files) to prevent duplicate HTML structure.

    Using the selector option: To prevent content from being patched into the wrong place when layouts are disabled, use the selector option. This sends a Datastar-Selector header, allowing Datastar to patch content into a specific container (e.g., #main-content) rather than replacing the entire page body.

    <!-- Example: SPA with specific selector for content updates -->
    <head>
      <%- datastar.scripts({ spa: { scope: 'nav', selector: 'main-content' } }) %>
    </head>
  9. Limitations of the PocketBase JSVM client

    main

    When using the pb() client in the JSVM environment, be aware of the following constraints:

    • Synchronous Only: All SDK methods are synchronous; async/await is not supported/needed.
    • No Realtime Subscriptions: You cannot use .subscribe() within hooks/JSVM.
    • No AsyncAuthStore: The AsyncAuthStore is not supported.
    • Caching: The client is cached after its first initialization.
  10. How realtime events work in Server-Side (JSVM) vs Client-Side

    main

    Realtime capabilities in PocketPages rely on WebSocket connections, but the capabilities differ based on the environment:

    • Server-Side (JSVM/Hooks): The JSVM environment does not support WebSocket connections, so you cannot subscribe to realtime events. You can only send realtime events to connected clients.
    • Client-Side (Browser/Node.js): Using the PocketBase JavaScript SDK, you can both send and receive realtime events.

    Always use the appropriate SDK for your environment: pocketbase-js-sdk-jsvm for server-side hooks and the standard pocketbase/js-sdk for client-side code.