ENbext.dev
DocumentationInclusions côté serveur

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

API du plugin

Les résolveurs intégrés gèrent les sources /__bext/*, les partiels du système de fichiers, les rendus V8 et les récupérations de proxy. Pour les sources spécifiques à un domaine (contenu CMS, lignes de base de données, API tierces), vous écrivez un résolveur personnalisé. L'API du plugin rend cela déclaratif via la correspondance de modèles et les hooks de cycle de vie.

rust
use bext_core::include::{IncludeResolver, IncludeDirective, IncludeContext, PluginResolver};

let cms_resolver = PluginResolver::new("/cms/*", |directive, ctx| {
    let slug = directive.src.strip_prefix("/cms/")?;
    let locale = ctx.vary_value("locale").unwrap_or("en");
    cms_client::fetch_block(slug, locale).ok()
});

chain_builder.with_plugin(Arc::new(cms_resolver));

Désormais, <bext-include src="/cms/homepage-hero"> est acheminé vers votre résolveur CMS. Mise en cache, authentification, variation – tout fonctionne car le résolveur se trouve dans le même pipeline.

Enregistrement du résolveur#

rust
PluginResolver::new(
    pattern,    // glob string: "/cms/*", "/products/*", "/api/feed/**"
    handler,    // Fn(&IncludeDirective, &IncludeContext) -> Option<String>
)

Le gestionnaire est appelé pour toute inclusion dont src correspond au modèle. Renvoie Some(html) en cas de succès, None à transmettre au résolveur suivant.

Spécificité du modèle#

Les résolveurs de plugins sont classés par spécificité de modèle (la plupart des caractères littéraux en premier). Les modèles plus spécifiques s'exécutent avant les modèles moins spécifiques :

code
/cms/posts/*      ← 10 literal chars, runs first
/cms/*            ← 4 literal chars
/*                ← 0 literal chars, runs last

Cela vous permet de superposer des gestionnaires spécialisés au-dessus d'un fourre-tout.

Syntaxe du modèle#

Même syntaxe globale que les règles de configuration :

  • * correspond à tous les caractères sauf /.
  • ** correspond à tous les séparateurs de chemin.
  • Les caractères littéraux correspondent littéralement.
Modèle Matchs
/cms/* /cms/post-1, /cms/hero
/cms/** /cms/post-1, /cms/posts/2025/jan
/api/v*/users /api/v1/users, /api/v2/users
Conseil

Les résolveurs de plugins se situent entre les résolveurs d'authentification/expérience et les résolveurs de contenu intégrés dans la chaîne. Cela signifie que l'authentification et la réécriture des sources des expériences se produisent automatiquement avant que votre résolveur ne soit appelé - vous n'avez pas besoin de les réimplémenter.

Crochets de cycle de vie#

Les plugins peuvent observer et transformer la résolution en quatre points :

rust
pub trait IncludeHook: Send + Sync {
    fn on_resolve(&self, directive: &mut IncludeDirective, ctx: &IncludeContext);
    fn on_resolved(&self, directive: &IncludeDirective, html: &mut String, ctx: &IncludeContext);
    fn on_cache_hit(&self, directive: &IncludeDirective, ctx: &IncludeContext);
    fn on_error(&self, directive: &IncludeDirective, error: &IncludeError, ctx: &IncludeContext);
}
Crochet Quand Mutable?
on_resolve Avant la résolution directif
on_resolved Après résolution, avant cache HTML
on_cache_hit En cas d'accès au cache (ignore la résolution) en lecture seule
on_error En cas d'échec de résolution en lecture seule

Les crochets se fixent sur un PluginResolver spécifique :

rust
let resolver = PluginResolver::new("/cms/*", handler)
    .with_hook(Arc::new(AnalyticsHook))
    .with_hook(Arc::new(MinifierHook));

Ou attaché dynamiquement :

rust
resolver.add_hook(Arc::new(DebugHook));

Modèles de crochet#

Analyse/observabilité :

rust
struct AnalyticsHook {
    metrics: Arc<MetricsClient>,
}

impl IncludeHook for AnalyticsHook {
    fn on_resolved(&self, d: &IncludeDirective, _html: &mut String, _ctx: &IncludeContext) {
        self.metrics.increment("include.resolved", &[("src", d.src.as_str())]);
    }

    fn on_error(&self, d: &IncludeDirective, err: &IncludeError, _ctx: &IncludeContext) {
        self.metrics.increment("include.error", &[
            ("src", d.src.as_str()),
            ("kind", err.kind()),
        ]);
    }
}

Transformation de sortie :

rust
struct MinifierHook;

impl IncludeHook for MinifierHook {
    fn on_resolved(&self, _d: &IncludeDirective, html: &mut String, _ctx: &IncludeContext) {
        *html = html_minifier::minify(html);
    }
}

Validation /application CSP :

rust
struct CspHook {
    allowed_patterns: Vec<String>,
}

impl IncludeHook for CspHook {
    fn on_resolve(&self, d: &mut IncludeDirective, _ctx: &IncludeContext) {
        let permitted = self.allowed_patterns.iter().any(|p| glob_match(p, &d.src));
        if !permitted {
            tracing::warn!(src = %d.src, "include blocked by CSP policy");
            d.src = "/_partials/csp-blocked.html".into();
        }
    }
}

Injection d'attributs :

rust
struct AnalyticsAttributeHook;

impl IncludeHook for AnalyticsAttributeHook {
    fn on_resolved(&self, d: &IncludeDirective, html: &mut String, _ctx: &IncludeContext) {
        // Add data-analytics-src to the root element.
        if let Some(pos) = html.find('>') {
            html.insert_str(pos, &format!(" data-analytics-src=\"{}\"", d.src));
        }
    }
}

Types d'erreurs#

Le hook on_error reçoit une erreur structurée :

rust
pub enum IncludeError {
    Unresolved,              // no resolver produced a result
    Forbidden,               // blocked by security policy
    Timeout,                 // resolution took too long
    CircuitOpen,             // circuit breaker skipped resolution
    Custom(String),          // resolver-specific error
}

Les erreurs sont uniquement destinées à l'observabilité : le pipeline gère automatiquement les chaînes de secours en fonction des attributs d'inclusion.

Intégration de la chaîne#

Enregistrez les plugins via le générateur de chaîne :

rust
use bext_core::include::{ChainBuilder, PluginResolver};

let chain = ChainBuilder::new()
    .with_cache(cache)
    .with_auth(true)
    .with_plugin(Arc::new(cms_resolver))
    .with_plugin(Arc::new(products_resolver))
    .with_plugin(Arc::new(legacy_api_resolver))
    .with_file(file_resolver)
    .with_fallback(true)
    .build();

Les résolveurs de plug-in se situent entre les résolveurs d'authentification/expérience et les résolveurs de contenu intégrés. Cela signifie que les plugins bénéficient automatiquement de l’authentification et de la réécriture des expériences, sans avoir à les réimplémenter.

Systèmes de plugins externes#

L'API du plugin fonctionne avec les environnements d'exécution des plugins WASM et V8 de bext. Un plugin WASM peut enregistrer des résolveurs d'inclusion via le manifeste du plugin :

toml
# plugin.toml
[plugin]
name = "cms-includes"
version = "1.0.0"

[include]
resolvers = [
  { pattern = "/cms/*",     handler = "resolve_cms" },
  { pattern = "/products/*", handler = "resolve_product" },
]
hooks = ["on_resolved"]

[include.config]
cms_api_url = { type = "string", required = true }
cms_api_key = { type = "string", required = true, secret = true }

[plugin.permissions]
network = ["api.contentful.com"]   # allowed outbound hosts
filesystem = []
cache = true
realtime = false

Le module WASM du plugin exporte les fonctions resolve_cms et resolve_product. bext charge le plug-in au démarrage, enregistre les résolveurs avec leurs modèles et achemine les inclusions correspondant au runtime WASM.

Voir Construire un plugin pour le guide complet de création du plugin.

Attention

Les plugins WASM et V8 s'exécutent sous les autorisations sandbox déclarées. Un plugin qui tente d'extraire un hôte ne figurant pas dans sa liste blanche network obtient une erreur d'autorisation au moment de l'exécution - et non une non-opération silencieuse. Déclarez tous les hôtes requis dans [plugin.permissions] avant le déploiement.

Autorisations du bac à sable#

Les résolveurs de plugins s'exécutent avec les autorisations déclarées :

toml
[plugin.permissions]
network = ["api.cms.example.com", "api.products.example.com"]
filesystem = []          # no FS access
cache = true             # can use bext.cache
realtime = false         # cannot publish to channels
secrets = ["cms_key"]    # can read specific secrets

Le bac à sable les applique au niveau d'exécution WASM/V8 : un plugin qui tente d'extraire un hôte non autorisé obtient une erreur d'autorisation, pas un échec silencieux.

Inscription dynamique#

Les plugins peuvent enregistrer des résolveurs au moment de l'exécution (pas seulement au démarrage) :

rust
#[bext_plugin::on_server_start]
fn init(ctx: &PluginContext) {
    let resolver = PluginResolver::new("/cms/*", handle_cms);
    ctx.register_include_resolver(Arc::new(resolver));
}

#[bext_plugin::on_reload]
fn on_config_reload(ctx: &PluginContext) {
    // Unregister old, register new — for hot-reloadable patterns.
}

C'est ainsi que vous créez des éléments tels que "Les éditeurs CMS peuvent ajouter de nouveaux modèles d'inclusion via une interface utilisateur d'administration sans déploiement".

Test des résolveurs de plugins#

Les résolveurs de plugins ne sont que des implémentations de IncludeResolver. Testez-les directement :

rust
#[test]
fn test_cms_resolver() {
    let r = PluginResolver::new("/cms/*", |d, _ctx| {
        Some(format!("[cms:{}]", d.src.strip_prefix("/cms/").unwrap()))
    });

    let d = IncludeDirective {
        src: "/cms/hero".into(),
        attrs: Default::default(),
        fallback_content: None,
    };
    let ctx = IncludeContext::new();

    assert_eq!(r.resolve(&d, &ctx).as_deref(), Some("[cms:hero]"));
}

Pour les tests d'intégration, construisez une petite chaîne avec votre résolveur et exercez le pipeline complet :

rust
let chain = ChainBuilder::new()
    .with_auth(false)
    .with_plugin(my_resolver)
    .with_fallback(true)
    .build();

let out = resolve_all(html, &chain, &IncludeContext::new());

Exemples pratiques#

Contentful / Sanity / Strapi inclut un résolveur#

rust
let cms = PluginResolver::new("/cms/*", move |d, ctx| {
    let slug = d.src.strip_prefix("/cms/")?;
    let locale = ctx.vary_value("locale").unwrap_or("en");
    let client = CMSClient::get();
    client.render_block(slug, locale).ok()
});

Fiche produit de la base de données#

rust
let products = PluginResolver::new("/products/*", move |d, _ctx| {
    let sku = d.src.strip_prefix("/products/")?;
    let product = db::fetch_product(sku).ok()?;
    Some(render_product_card(&product))
});

API tierce avec mise en forme des réponses#

rust
let stripe_widget = PluginResolver::new("/stripe/pricing*", move |_d, ctx| {
    let user_id = ctx.session.as_ref()?.user_id.as_ref()?;
    let customer = stripe::get_customer(user_id).ok()?;
    Some(render_pricing_for(&customer))
});

Routage de l'API héritée#

rust
let legacy = PluginResolver::new("/legacy-api/*", |d, _ctx| {
    let path = d.src.strip_prefix("/legacy-api/")?;
    let url = format!("https://old-api.internal.example.com/{}", path);
    http::get(&url).ok().and_then(|r| r.text().ok())
});

Voir aussi#

Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.