en-US/about_Hyde_Plugin_Authoring.help.txt

TOPIC
    about_hyde_plugin_authoring
 
SHORT DESCRIPTION
    Guidance for authoring Hyde plugins.
 
LONG DESCRIPTION
    Hyde plugins are PowerShell scripts that return a descriptor hashtable.
    They can:
    - register Hyde lifecycle hooks
    - register Liquid tags and filters through PowerLiquid's extension registry
    - influence output paths
    - enrich document metadata before rendering
    Hyde loads plugins from the configured `plugins_dir`, which defaults to
    `_plugins`. It also ships with built-in plugins under `src/Plugins`.
 
Plugin File Shape
    A plugin should be documented with a comment-based help block near the top
    of the file.
    Every plugin should expose an `-Install` switch so installation/setup
    behavior is self-contained in the plugin script.
    A plugin script should return a hashtable:
 
    [CmdletBinding()]
    param(
        $Context,
        [switch]$Install
    )
     
    if ($Install) {
        Write-Verbose "No installation is required for this plugin."
        return $true
    }
     
    @{
        Name = 'my-plugin'
        Hooks = @{
            BeforeRenderDocument = {
                param($Invocation)
     
                $document = $Invocation.Document
                if ($null -eq $document) {
                    return
                }
     
                $document.FrontMatter['example'] = 'value'
            }
        }
        Liquid = @{
            Tags = @{
                hello = {
                    param($Invocation)
                    return 'Hello from a Hyde plugin'
                }
            }
            Filters = @{
                reverse_words = {
                    param($Value, $Arguments, $Invocation)
     
                    if ($null -eq $Value) {
                        return ''
                    }
     
                    $words = @(([string]$Value) -split '\s+')
                    [array]::Reverse($words)
                    return ($words -join ' ')
                }
            }
        }
    }
 
Context Parameter
    The top-level `param($Context)` receives the active `HydeBuildContext`.
    Use it to inspect:
    - site settings
    - source and destination paths
    - the Hyde version
    - the shared Liquid extension registry
    Most plugins will only need `$Context` for setup-time decisions. Hook
    handlers receive their own invocation object later.
 
Install Contract
    Hyde plugin scripts should implement an install path using `-Install`.
    - When a plugin has no setup dependencies, `-Install` should return `True`.
    - If `-Verbose` is supplied, `-Install` should state installation is not needed.
    - When a plugin has dependencies (for example external binaries), `-Install`
       should fetch or prepare those dependencies from inside the plugin script.
    This keeps plugin setup and plugin behavior in one place.
 
Hook Names
    Current Hyde hook points:
    - `AfterInitialize`
    - `AfterDiscoverDocument`
    - `AfterDiscoverStaticFile`
    - `BeforeRenderDocument`
    - `AfterRenderDocument`
    - `BeforeWriteDocument`
    - `AfterWriteDocument`
    - `BeforeCopyStaticFile`
    - `AfterCopyStaticFile`
    - `ResolveDocumentOutputPath`
    - `ResolveStaticFileOutputPath`
    These names match the registry in
    src/Private/Hyde.Plugins.ps1
    .
 
Hook Invocation Shape
    Hook handlers receive a single `$Invocation` object. Depending on the hook,
    it may contain:
    - `Context`
    - `Document`
    - `StaticFile`
    - `OutputPath`
    - `DestinationPath`
    - `CancelCopy` (mutable flag for static-file transform hooks)
    Value resolver hooks (`ResolveDocumentOutputPath`,
    `ResolveStaticFileOutputPath`) receive two parameters:
 
    param($CurrentValue, $Invocation)
 
    For `BeforeCopyStaticFile`, handlers can set:
 
    $Invocation.CancelCopy = $true
 
    When `CancelCopy` is true, Hyde skips the default `Copy-Item` step. This is
    useful when your plugin writes transformed output itself (for example, SCSS
    -> CSS).
    For document-centric hooks, you should expect `Document` to be a
    `HydeDocument` object with semantic properties such as:
    - `Title`
    - `Url`
    - `OutputPath`
    - `FrontMatter`
    - `RawContent`
    - `RenderedContent`
    - `Collection`
    - `PostDate`
 
Liquid Extensions
    Hyde plugins do not ask PowerLiquid to load plugins directly.
    Instead, Hyde:
    1. discovers plugin scripts 2. reads their descriptor 3. registers any
    declared Liquid tags and filters into the current PowerLiquid registry 4.
    passes that registry into `Invoke-LiquidTemplate`
    That separation keeps PowerLiquid reusable in other hosts while still
    letting Hyde plugins extend Liquid behavior.
 
    SIMPLE CUSTOM TAG
    param($Context)
     
    @{
        Name = 'hello-tag'
        Liquid = @{
            Tags = @{
                hello = {
                    param($Invocation)
                    return 'Hello from Paul'
                }
            }
        }
    }
 
    Usage:
 
    {% hello %}
 
    Should return `Hello from Paul`
 
    SIMPLE CUSTOM FILTER
    param($Context)
     
    @{
        Name = 'shout-filter'
        Liquid = @{
            Filters = @{
                shout = {
                    param($Value, $Arguments, $Invocation)
                    return ([string]$Value).ToUpperInvariant() + '!'
                }
            }
        }
    }
 
    Usage:
 
    {{ page.title | shout }}
 
    Running against a page with title 'Hello' should return `HELLO!`
 
Built-In Plugin Examples
    Use these as reference implementations:
-
    src/Plugins/seo-tag.ps1
    -
    src/Plugins/titles-from-headings.ps1
    `seo-tag` shows a custom Liquid tag
    `titles-from-headings` shows a document lifecycle hook that enriches
    semantic document metadata
 
Plugin Discovery Rules
    If a configured name starts with `jekyll-`, Hyde also tries:
    - the original name
    - the name with `jekyll-` removed
    - the stripped name prefixed with `hyde-`
    That makes names like `jekyll-seo-tag` resolve cleanly to Hyde-friendly
    implementations.
 
Safe Mode
    When `safe: true` is enabled in site configuration, Hyde restricts plugin
    loading to the explicit `whitelist` set. This is useful for locked-down
    environments, CI builds, or untrusted content where arbitrary plugin
    execution is a risk.
    - `safe: true` means other plugin IDs are ignored, whether configured in
    `_config.yml` `plugins:` or present in `_plugins`/`src/Plugins`.
    - `whitelist` should be an array of identifiers that correspond to plugin
    `Name` values (e.g. `seo-tag`, `titles-from-headings`).
    - If `whitelist` is empty or missing, no plugins are loaded while safe mode
    is active.
    - Mixed-mode behavior: safe mode only affects plugin whitelist filtering;
    non-plugin features (e.g., core rendering and file discovery) still run.
    Example:
 
    safe: true
    plugins:
      - seo-tag
      - custom-plugin
    whitelist:
      - seo-tag
 
    In this config, `custom-plugin` is ignored because it is not whitelisted.
 
Recommendations
    - Prefer mutating semantic document properties such as `document.title`
    instead of only changing rendered HTML.
    - Keep hooks narrowly scoped and idempotent.
    - Use Liquid tags and filters for presentation behavior.
    - Use Hyde hooks for discovery, metadata enrichment, or output-path changes.
    - Avoid directly reading or writing arbitrary files unless the plugin truly
    owns that behavior.
    - Document your code.
 
Example: Transform Static Assets
    This pattern remaps `.scss` output to `.css`, writes transformed output,
    then cancels the raw source copy.
 
    param($Context)
     
    @{
        Name = 'example-transform'
        Hooks = @{
            ResolveStaticFileOutputPath = {
                param($CurrentValue, $Invocation)
     
                if ($Invocation.StaticFile.Extension -eq '.scss') {
                    return ([System.IO.Path]::ChangeExtension($CurrentValue, '.css').Replace('\\', '/'))
                }
     
                return $CurrentValue
            }
     
            BeforeCopyStaticFile = {
                param($Invocation)
     
                if ($Invocation.StaticFile.Extension -ne '.scss') {
                    return
                }
     
                $destinationPath = Join-Path -Path $Invocation.Context.DestinationPath -ChildPath $Invocation.StaticFile.OutputRelativePath
                $destinationDirectory = Split-Path -Path $destinationPath -Parent
                if (-not (Test-Path -LiteralPath $destinationDirectory -PathType Container)) {
                    [void](New-Item -Path $destinationDirectory -ItemType Directory -Force)
                }
     
                Set-Content -LiteralPath $destinationPath -Encoding UTF8 -Value '/* transformed css */'
                $Invocation.CancelCopy = $true
            }
        }
    }