Movatterモバイル変換


[0]ホーム

URL:


Skip to content
📢 Looking for BunkerWeb CLOUD, PRO version, technical support or tailored services ? Visit theBunkerWeb Panel for more information on our enterprise offers.

Plugins

BunkerWeb comes with a plugin system making it possible to easily add new features. Once a plugin is installed, you can manage it using additional settings defined by the plugin.

Official plugins

Here is the list of "official" plugins that we maintain (see thebunkerweb-plugins repository for more information) :

NameVersionDescriptionLink
ClamAV1.9Automatically scans uploaded files with the ClamAV antivirus engine and denies the request when a file is detected as malicious.bunkerweb-plugins/clamav
Coraza1.9Inspect requests using the Coraza WAF (alternative of ModSecurity).bunkerweb-plugins/coraza
Discord1.9Send security notifications to a Discord channel using a Webhook.bunkerweb-plugins/discord
Slack1.9Send security notifications to a Slack channel using a Webhook.bunkerweb-plugins/slack
VirusTotal1.9Automatically scans uploaded files with the VirusTotal API and denies the request when a file is detected as malicious.bunkerweb-plugins/virustotal
WebHook1.9Send security notifications to a custom HTTP endpoint using a Webhook.bunkerweb-plugins/webhook

How to use a plugin

Automatic

If you want to quickly install external plugins, you can use theEXTERNAL_PLUGIN_URLS setting. It takes a list of URLs separated by spaces, each pointing to a compressed (zip format) archive containing one or more plugins.

You can use the following value if you want to automatically install the official plugins :EXTERNAL_PLUGIN_URLS=https://github.com/bunkerity/bunkerweb-plugins/archive/refs/tags/v1.9.zip

Manual

The first step is to install the plugin by placing its files inside the correspondingplugins data folder. The procedure depends on your integration :

When using theDocker integration, plugins must be placed in the volume mounted on/data/plugins in the scheduler container.

The first thing to do is to create the plugins folder :

mkdir-p./bw-data/plugins

Then, you can drop the plugins of your choice into that folder :

gitclonehttps://github.com/bunkerity/bunkerweb-plugins&&\cp-rp./bunkerweb-plugins/*./bw-data/plugins
Using local folder for persistent data

The scheduler runs as anunprivileged user with UID 101 and GID 101 inside the container. The reason behind this is security : in case a vulnerability is exploited, the attacker won't have full root (UID/GID 0) privileges.But there is a downside : if you use alocal folder for the persistent data, you will need toset the correct permissions so the unprivileged user can write data to it. Something like that should do the trick :

mkdirbw-data&&\chownroot:101bw-data&&\chmod770bw-data

Alternatively, if the folder already exists :

chown-Rroot:101bw-data&&\chmod-R770bw-data

If you are usingDocker in rootless mode orpodman, UIDs and GIDs in the container will be mapped to different ones in the host. You will first need to check your initial subuid and subgid :

grep^$(whoami):/etc/subuid&&\grep^$(whoami):/etc/subgid

For example, if you have a value of100000, the mapped UID/GID will be100100 (100000 + 100) :

mkdirbw-data&&\sudochgrp100100bw-data&&\chmod770bw-data

Or if the folder already exists :

sudochgrp-R100100bw-data&&\chmod-R770bw-data

Then you can mount the volume when starting your Docker stack :

services:...bw-scheduler:image:bunkerity/bunkerweb-scheduler:1.6.5volumes:-./bw-data:/data...

When using theDocker autoconf integration, plugins must be placed in the volume mounted on/data/plugins in the scheduler container.

The first thing to do is to create the plugins folder :

mkdir-p./bw-data/plugins

Then, you can drop the plugins of your choice into that folder :

gitclonehttps://github.com/bunkerity/bunkerweb-plugins&&\cp-rp./bunkerweb-plugins/*./bw-data/plugins

Because the scheduler runs as an unprivileged user with UID and GID 101, you will need to edit the permissions :

chown-R101:101./bw-data

Then you can mount the volume when starting your Docker stack :

services:...bw-scheduler:image:bunkerity/bunkerweb-scheduler:1.6.5volumes:-./bw-data:/data...

Deprecated

The Swarm integration is deprecated and will be removed in a future release. Please consider using theKubernetes integration instead.

More information can be found in theSwarm integration documentation.

When using theSwarm integration, plugins must be placed in the volume mounted on/data/plugins in the scheduler container.

Swarm volume

Configuring a Swarm volume that will persist when the scheduler service is running on different nodes is not covered is in this documentation. We will assume that you have a shared folder mounted on/shared across all nodes.

The first thing to do is to create the plugins folder :

mkdir-p/shared/bw-plugins

Then, you can drop the plugins of your choice into that folder :

gitclonehttps://github.com/bunkerity/bunkerweb-plugins&&\cp-rp./bunkerweb-plugins/*/shared/bw-plugins

Because the scheduler runs as an unprivileged user with UID and GID 101, you will need to edit the permissions :

chown-R101:101/shared/bw-plugins

Then you can mount the volume when starting your Swarm stack :

services:...bw-scheduler:image:bunkerity/bunkerweb-scheduler:1.6.5volumes:-/shared/bw-plugins:/data/plugins...

When using theKubernetes integration, plugins must be placed in the volume mounted on/data/plugins in the scheduler container.

The first thing to do is to declare aPersistentVolumeClaim that will contain our plugins data :

apiVersion:v1kind:PersistentVolumeClaimmetadata:name:pvc-bunkerweb-pluginsspec:accessModes:-ReadWriteOnceresources:requests:storage:5Gi

You can now add the volume mount and an init container to automatically provision the volume :

apiVersion:apps/v1kind:Deploymentmetadata:name:bunkerweb-schedulerspec:replicas:1strategy:type:Recreateselector:matchLabels:app:bunkerweb-schedulertemplate:metadata:labels:app:bunkerweb-schedulerspec:serviceAccountName:sa-bunkerwebcontainers:-name:bunkerweb-schedulerimage:bunkerity/bunkerweb-scheduler:1.6.5imagePullPolicy:Alwaysenv:-name:KUBERNETES_MODEvalue:"yes"-name:"DATABASE_URI"value:"mariadb+pymysql://bunkerweb:changeme@svc-bunkerweb-db:3306/db"volumeMounts:-mountPath:"/data/plugins"name:vol-pluginsinitContainers:-name:bunkerweb-scheduler-initimage:alpine/gitcommand:["/bin/sh","-c"]args:["gitclonehttps://github.com/bunkerity/bunkerweb-plugins/data/plugins&&chown-R101:101/data/plugins"]volumeMounts:-mountPath:"/data/plugins"name:vol-pluginsvolumes:-name:vol-pluginspersistentVolumeClaim:claimName:pvc-bunkerweb-plugins

When using theLinux integration, plugins must be written to the/etc/bunkerweb/plugins folder :

gitclonehttps://github.com/bunkerity/bunkerweb-plugins&&\cp-rp./bunkerweb-plugins/*/etc/bunkerweb/plugins&&\chown-Rnginx:nginx/etc/bunkerweb/plugins

Writing a plugin

Structure

Existing plugins

If the documentation is not enough, you can have a look at the existing source code ofofficial plugins and thecore plugins (already included in BunkerWeb but they are plugins, technically speaking).

What a plugin structure looks like:

plugin /    confs / conf_type / conf_name.conf    ui / actions.py         hooks.py         template.html         blueprints / <blueprint_file(s)>              templates / <blueprint_template(s)>    jobs / my-job.py    bwcli / my-command.py    templates / my-template.json          my-template / configs / conf_type / conf_name.conf    plugin.lua    plugin.json

  • conf_name.conf : Addcustom NGINX configurations (as Jinja2 templates).

  • actions.py : Script to execute on the Flask server. This script runs in a Flask context, giving you access to libraries and utilities likejinja2 andrequests.

  • hooks.py : Custom Python file that contains flask's hooks and will be executed when the plugin is loaded.

  • template.html : Custom plugin page accessed via the UI.

  • blueprints folder (within ui): This folder is used to override existing Flask blueprints or create new ones. Inside, you can include blueprint files and an optionaltemplates subfolder for blueprint-specific templates.

  • jobs py file : Custom Python files executed as jobs by the scheduler.

  • bwcli folder : Python (or executable) files that extend thebwcli CLI through custom commands.

  • my-template.json : Addcustom templates to override the default values of settings and apply custom configurations easily.

  • plugin.lua : Code executed on NGINX using theNGINX LUA module.

  • plugin.json : Metadata, settings, and job definitions for your plugin.

Getting started

The first step is to create a folder that will contain the plugin :

mkdirmyplugin&&\cdmyplugin

Metadata

A file namedplugin.json and written at the root of the plugin folder must contain metadata about the plugin. Here is an example :

{"id":"myplugin","name":"My Plugin","description":"Just an example plugin.","version":"1.0","stream":"partial","settings":{"DUMMY_SETTING":{"context":"multisite","default":"1234","help":"Here is the help of the setting.","id":"dummy-id","label":"Dummy setting","regex":"^.*$","type":"text"}},"jobs":[{"name":"my-job","file":"my-job.py","every":"hour"}]}

Here are the details of the fields :

FieldMandatoryTypeDescription
idyesstringInternal ID for the plugin : must be unique among other plugins (including "core" ones) and contain only lowercase chars.
nameyesstringName of your plugin.
descriptionyesstringDescription of your plugin.
versionyesstringVersion of your plugin.
streamyesstringInformation about stream support :no,yes orpartial.
settingsyesdictList of the settings of your plugin.
jobsnolistList of the jobs of your plugin.
bwclinodictMap CLI command names to files stored in the plugin'sbwcli directory to expose CLI plugins.

Each setting has the following fields (the key is the ID of the settings used in a configuration) :

FieldMandatoryTypeDescription
contextyesstringContext of the setting :multisite orglobal.
defaultyesstringThe default value of the setting.
helpyesstringHelp text about the plugin (shown in web UI).
idyesstringInternal ID used by the web UI for HTML elements.
labelyesstringLabel shown by the web UI.
regexyesstringThe regex used to validate the value provided by the user.
typeyesstringThe type of the field :text,check,select orpassword.
multiplenostringUnique ID to group multiple settings with numbers as suffix.
selectnolistList of possible string values whentype isselect.

Each job has the following fields :

FieldMandatoryTypeDescription
nameyesstringName of the job.
fileyesstringName of the file inside the jobs folder.
everyyesstringJob scheduling frequency :minute,hour,day,week oronce (no frequency, only once before (re)generating the configuration).

CLI commands

Plugins can extend thebwcli tool with custom commands that run underbwcli plugin <plugin_id> ...:

  1. Add abwcli directory in your plugin and drop one file per command (for examplebwcli/list.py). The CLI adds the plugin path tosys.path before executing the file.
  2. Declare the commands in the optionalbwcli section ofplugin.json, mapping each command name to its executable file name.
{"bwcli":{"list":"list.py","save":"save.py"}}

The scheduler automatically exposes the declared commands once the plugin is installed. Core plugins, such asbackup insrc/common/core/backup, follow the same pattern.

Configurations

You can add custom NGINX configurations by adding a folder namedconfs with content similar to thecustom configurations. Each subfolder inside theconfs will containjinja2 templates that will be generated and loaded at the corresponding context (http,server-http,default-server-http,stream,server-stream,modsec,modsec-crs,crs-plugins-before andcrs-plugins-after).

Here is an example for a configuration template file inside theconfs/server-http folder namedexample.conf :

location/setting{default_type'text/plain';content_by_lua_block{ngx.say('{{DUMMY_SETTING}}')}}

{{ DUMMY_SETTING }} will be replaced by the value of theDUMMY_SETTING chosen by the user of the plugin.

Templates

Check thetemplates documentation for more information.

LUA

Main script

Under the hood, BunkerWeb is using theNGINX LUA module to execute code within NGINX. Plugins that need to execute code must provide a lua file at the root directory of the plugin folder using theid value ofplugin.json as its name. Here is an example namedmyplugin.lua :

localclass=require"middleclass"localplugin=require"bunkerweb.plugin"localutils=require"bunkerweb.utils"localmyplugin=class("myplugin",plugin)functionmyplugin:initialize(ctx)plugin.initialize(self,"myplugin",ctx)self.dummy="dummy"endfunctionmyplugin:init()self.logger:log(ngx.NOTICE,"init called")returnself:ret(true,"success")endfunctionmyplugin:set()self.logger:log(ngx.NOTICE,"set called")returnself:ret(true,"success")endfunctionmyplugin:access()self.logger:log(ngx.NOTICE,"access called")returnself:ret(true,"success")endfunctionmyplugin:log()self.logger:log(ngx.NOTICE,"log called")returnself:ret(true,"success")endfunctionmyplugin:log_default()self.logger:log(ngx.NOTICE,"log_default called")returnself:ret(true,"success")endfunctionmyplugin:preread()self.logger:log(ngx.NOTICE,"preread called")returnself:ret(true,"success")endfunctionmyplugin:log_stream()self.logger:log(ngx.NOTICE,"log_stream called")returnself:ret(true,"success")endreturnmyplugin

The declared functions are automatically called during specific contexts. Here are the details of each function :

FunctionContextDescriptionReturn value
initinit_by_luaCalled when NGINX just started or received a reload order. the typical use case is to prepare any data that will be used by your plugin.ret,msg
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
setset_by_luaCalled before each request received by the server.The typical use case is for computing before access phase.ret,msg
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
accessaccess_by_luaCalled on each request received by the server. The typical use case is to do the security checks here and deny the request if needed.ret,msg,status,redirect
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
  • status (number) : interrupt current process and returnHTTP status
  • redirect (URL) : if set will redirect to given URL
loglog_by_luaCalled when a request has finished (and before it gets logged to the access logs). The typical use case is to make stats or compute counters for example.ret,msg
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
log_defaultlog_by_luaSame aslog but only called on the default server.ret,msg
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
prereadpreread_by_luaSimilar to theaccess function but for stream mode.ret,msg,status
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message
  • status (number) : interrupt current process and returnstatus
log_streamlog_by_luaSimilar to thelog function but for stream mode.ret,msg
  • ret (boolean) : true if no error or else false
  • msg (string) : success or error message

Libraries

All directives fromNGINX LUA module and are available andNGINX stream LUA module. On top of that, you can use the LUA libraries included within BunkerWeb : seethis script for the complete list.

If you need additional libraries, you can put them in the root folder of the plugin and access them by prefixing them with your plugin ID. Here is an example file namedmylibrary.lua :

local_M={}_M.dummy=function()return"dummy"endreturn_M

And here is how you can use it from themyplugin.lua file :

localmylibrary=require"myplugin.mylibrary"...mylibrary.dummy()...

Helpers

Some helpers modules provide common helpful helpers :

  • self.variables : allows to access and store plugins' attributes
  • self.logger : print logs
  • bunkerweb.utils : various useful functions
  • bunkerweb.datastore : access the global shared data on one instance (key/value store)
  • bunkerweb.clusterstore : access a Redis data store shared between BunkerWeb instances (key/value store)

To access the functions, you first need torequire the modules :

localutils=require"bunkerweb.utils"localdatastore=require"bunkerweb.datastore"localclustestore=require"bunkerweb.clustertore"

Retrieve a setting value :

localmyvar=self.variables["DUMMY_SETTING"]ifnotmyvarthenself.logger:log(ngx.ERR,"can't retrieve setting DUMMY_SETTING")elseself.logger:log(ngx.NOTICE,"DUMMY_SETTING = "..value)end

Store something in the local cache :

localok,err=self.datastore:set("plugin_myplugin_something","somevalue")ifnotokthenself.logger:log(ngx.ERR,"can't save plugin_myplugin_something into datastore : "..err)elseself.logger:log(ngx.NOTICE,"successfully saved plugin_myplugin_something into datastore")end

Check if an IP address is global :

localret,err=utils.ip_is_global(ngx.ctx.bw.remote_addr)ifret==nilthenself.logger:log(ngx.ERR,"error while checking if IP "..ngx.ctx.bw.remote_addr.." is global or not : "..err)elseifnotretthenself.logger:log(ngx.NOTICE,"IP "..ngx.ctx.bw.remote_addr.." is not global")elseself.logger:log(ngx.NOTICE,"IP "..ngx.ctx.bw.remote_addr.." is global")end

More examples

If you want to see the full list of available functions, you can have a look at the files present in thelua directory of the repository.

Jobs

BunkerWeb uses an internal job scheduler for periodic tasks like renewing certificates with certbot, downloading blacklists, downloading MMDB files, ... You can add tasks of your choice by putting them inside a subfolder namedjobs and listing them in theplugin.json metadata file. Don't forget to add the execution permissions for everyone to avoid any problems when a user is cloning and installing your plugin.

Plugin page

Everything related to the web UI is located inside the subfolderui as we seen in theprevious structure section..

Prerequisites

When you want to create a plugin page, you need two files :

  • template.html that will be accessible with aGET /plugins/<plugin_id>.

  • actions.py where you can add some scripting and logic with aPOST /plugins/<plugin_id>. Notice that this fileneed a function with the same name as the plugin to work. This file is needed even if the function is empty.

Basic example

Jinja 2 template

Thetemplate.html file is a Jinja2 template, please refer to theJinja2 documentation if needed.

We can put aside theactions.py file and startonly using the template on a GET situation. The template can access app context and libs, so you can use Jinja, request or flask utils.

For example, you can get the request arguments in your template like this :

<p>request args : {{ request.args.get() }}.</p>

Actions.py

CSRF Token

Please note that every form submission is protected via a CSRF token, you will need to include the following snippet into your forms :

<inputtype="hidden"name="csrf_token"value="{{ csrf_token() }}"/>

You can power-up your plugin page with additional scripting with theactions.py file when sending aPOST /plugins/<plugin_id>.

You have two functions by default inactions.py :

pre_render function

This allows you to retrieve data when youGET the template, and to use the data with the pre_render variable available in Jinja to display content more dynamically.

defpre_render(**kwargs)return<pre_render_data>

BunkerWeb will send you this type of response :

returnjsonify({"status":"ok|ko","code":XXX,"data":<pre_render_data>}),200

<plugin_id> function

This allows you to retrieve data when you make aPOST from the template endpoint, which must be used in AJAX.

defmyplugin(**kwargs)return<plugin_id_data>

BunkerWeb will send you this type of response :

returnjsonify({"message":"ok","data":<plugin_id_data>}),200

What you can access from action.py

Here are the arguments that are passed and access on action.py functions:

function(app=app,args=request.args.to_dict()orrequest.jsonorNone)

Available Python Libraries

BunkerWeb's Web UI includes a set of pre-installed Python libraries that you can use in your plugin'sactions.py or other UI-related scripts. These are available out-of-the-box without needing additional installations.

Here's the complete list of included libraries:

  • bcrypt - Password hashing library
  • biscuit-python - Biscuit authentication tokens
  • certbot - ACME client for Let's Encrypt
  • Flask - Web framework
  • Flask-Login - User session management
  • Flask-Session[cachelib] - Server-side session storage
  • Flask-WTF - Form handling and CSRF protection
  • gunicorn[gthread] - WSGI HTTP server
  • pillow - Image processing
  • psutil - System and process utilities
  • python_dateutil - Date and time utilities
  • qrcode - QR code generation
  • regex - Advanced regular expressions
  • urllib3 - HTTP client
  • user_agents - User agent parsing

Using Libraries in Your Plugin

To import and use these libraries in youractions.py file, simply use the standard Pythonimport statement. For example:

fromflaskimportrequestimportbcrypt
External Libraries

If you need libraries not listed above, install them inside theui folder of your plugin and import them using the classicalimport directive. Ensure compatibility with the existing environment to avoid conflicts.

Some examples

  • Retrieve form submitted data
fromflaskimportrequestdefmyplugin(**kwargs):my_form_value=request.form["my_form_input"]returnmy_form_value
  • Access app config

action.py

fromflaskimportrequestdefpre_render(**kwargs):config=kwargs['app'].config["CONFIG"].get_config(methods=False)returnconfig

template

<!-- metadata + config --><div>{{ pre_render }}</div>

Hooks.py

This documentation outlines the lifecycle hooks used for managing different stages of a request within the application. Each hook is associated with a specific phase.

These hooks are executedbefore processing an incoming request. They are typically used for pre-processing tasks such as authentication, validation, or logging.

If the hook returns a response object, Flask will skip the request handling and return the response directly. This can be useful for short-circuiting the request processing pipeline.

Example:

fromflaskimportrequest,Responsedefbefore_request():print("Before-request: Validating request...",flush=True)# Perform authentication, validation, or logging hereifnotis_valid_request(request):# We are in the app contextreturnResponse("Invalid request!",status=400)defis_valid_request(request):# Dummy validation logicreturn"user"inrequest

These hooks that runafter the request has been processed. They are ideal for post-processing tasks such as cleanup, additional logging, or modifying the response before it is sent back.

They receive the response object as an argument and can modify it before returning it. The first after_request hook to return a response will be used as the final response.

Example:

fromflaskimportrequestdefafter_request(response):print("After-request: Logging response...",flush=True)# Perform logging, cleanup, or response modifications herelog_response(response)returnresponsedeflog_response(response):# Dummy logging logicprint("Response logged:",response,flush=True)

These hooks are invoked when the request context is being torn down. These hooks are used for releasing resources or handling errors that occurred during the request lifecycle.

Example:

defteardown_request(error=None):print("Teardown-request: Cleaning up resources...",flush=True)# Perform cleanup, release resources, or handle errors hereiferror:handle_error(error)cleanup_resources()defhandle_error(error):# Dummy error handling logicprint("Error encountered:",error,flush=True)defcleanup_resources():# Dummy resource cleanup logicprint("Resources have been cleaned up.",flush=True)

These hooks are used to inject additional context into templates or views. They enrich the runtime context by passing common data (like user information or configuration settings) to the templates.

If a context processor returns a dictionary, the keys and values will be added to the context for all templates. This allows you to share data across multiple views or templates.

Example:

defcontext_processor()->dict:print("Context-processor: Injecting context data...",flush=True)# Return a dictionary containing context data for templates/viewsreturn{"current_user":"John Doe","app_version":"1.0.0","feature_flags":{"new_ui":True}}

This lifecycle hook design provides a modular and systematic approach to managing various aspects of a request's lifecycle:

  • Modularity: Each hook is responsible for a distinct phase, ensuring that concerns are separated.
  • Maintainability: Developers can easily add, modify, or remove hook implementations without impacting other parts of the request lifecycle.
  • Extensibility: The framework is flexible, allowing for additional hooks or enhancements as application requirements evolve.

By clearly defining the responsibilities of each hook and their associated logging prefixes, the system ensures that each stage of request processing is transparent and manageable.

Blueprints

In Flask,blueprints serve as a modular way to organize related components—such as views, templates, and static files—within your application. They allow you to group functionality logically and can be used to create new sections of your app or override existing ones.

Creating a Blueprint

To define a blueprint, you create an instance of theBlueprint class, specifying its name and import path. You then define routes and views associated with this blueprint.

Example: Defining a New Blueprint

fromos.pathimportdirnamefromflaskimportBlueprint,render_template# Define the blueprintmy_blueprint=Blueprint('my_blueprint',__name__,template_folder=dirname(__file__)+'/templates')# The template_folder is set to avoid conflicts with the original blueprint# Define a route within the blueprint@my_blueprint.route('/my_blueprint')defmy_blueprint_page():returnrender_template('my_blueprint.html')

In this example, a blueprint namedmy_blueprint is created, and a route/my_blueprint is defined within it.

Overriding an Existing Blueprint

Blueprints can also override existing ones to modify or extend functionality. To do this, ensure that the new blueprint has the same name as the one you're overriding and register it after the original.

Example: Overriding an Existing Blueprint

fromos.pathimportdirnamefromflaskimportFlask,Blueprint# Original blueprintinstances=Blueprint('instances',__name__,template_folder=dirname(__file__)+'/templates')# The template_folder is set to avoid conflicts with the original blueprint@instances.route('/instances')defoverride_instances():return"My new instances page"

In this scenario, accessing the URL/instances will display "My new instances page" because theinstances blueprint, registered last, overrides the originalinstances blueprint.

About overriding

Be cautious when overriding existing blueprints, as it can impact the behavior of the application. Ensure that the changes align with the application's requirements and do not introduce unexpected side effects.

All existing routes will be removed from he original blueprint, so you will need to re-implement them if needed.

Naming Conventions

Important

Ensure the blueprint’s name matches the blueprint variable name, else it will not be considered as a valid blueprint and will not be registered.

For consistency and clarity, it's advisable to follow these naming conventions:

  • Blueprint Names: Use short, all-lowercase names. Underscores can be used for readability, e.g.,user_auth.

  • File Names: Match the filename to the blueprint name, ensuring it's all lowercase with underscores as needed, e.g.,user_auth.py.

This practice aligns with Python's module naming conventions and helps maintain a clear project structure.

Example: Blueprint and File Naming

plugin /    ui / blueprints / user_auth.py                      templates / user_auth.html

In this structure,user_auth.py contains theuser_auth blueprint, anduser_auth.html is the associated template, adhering to the recommended naming conventions.


[8]ページ先頭

©2009-2025 Movatter.jp