33Config files are rendered through a two-stage pipeline:
44
55```
6- template source → Pongo2 render → JSON5 strip → [base merge] → valid JSON → core process
6+ template source → Pongo2 render → JSON5 strip → valid JSON → core process
7+ ↑ ↑
8+ extends/block/include skipped when strip_json5:false
9+ resolves automatically
710```
811
912---
@@ -48,14 +51,30 @@ Both forms work:
4851}
4952```
5053
54+ ### Environment variable access
55+
56+ Templates can read OS environment variables via the ` env ` map:
57+
58+ ``` json5
59+ " private_key" : " {{ env.WG_SERVER_PRIVKEY }}" ,
60+ " api_token" : " {{ env.MY_API_KEY }}" ,
61+ {% if env .DEBUG % }" log_level" : " debug" ,{% endif % }
62+ ```
63+
64+ Missing env vars render as empty string — no error.
65+
5166### Loops
5267
53- Pass a structured context via the runner (future extension) for loops :
68+ Use ` |split ` filter to iterate over comma-separated strings :
5469
5570``` json5
56- " users" : [
57- {% for u in users % }
58- {" uuid" : " {{ u.uuid }}" , " email" : " {{ u.email }}" },
71+ // run.json.j2 — auto-generate variant list
72+ " vars" : [
73+ {% for flow in " ,xtls-rprx-vision" | split: " ," % }
74+ {
75+ " TITLE" : " {% if flow %}vless-flow{% else %}plain-tls{% endif %}" ,
76+ " VLESS_FLOW" : " {{ flow }}"
77+ },
5978 {% endfor % }
6079]
6180```
@@ -64,15 +83,14 @@ Pass a structured context via the runner (future extension) for loops:
6483
6584< https://github.com/flosch/pongo2?tab=readme-ov-file#filters >
6685
67- Common ones:
68-
6986| Filter | Example | Result |
7087| ---| ---| ---|
7188| ` default ` | ` {{ X\|default:"foo" }} ` | ` foo ` if X empty |
7289| ` lower ` | ` {{ NAME\|lower }} ` | lowercase |
7390| ` upper ` | ` {{ NAME\|upper }} ` | UPPERCASE |
7491| ` truncatechars ` | ` {{ S\|truncatechars:8 }} ` | first 8 chars |
7592| ` replace ` | ` {{ S\|replace:"a":"b" }} ` | char replace |
93+ | ` split ` | ` {{ "a,b"\|split:"," }} ` | list (for ` {% for %} ` ) |
7694
7795---
7896
@@ -98,35 +116,79 @@ standard JSON that proxy cores accept.
98116}
99117```
100118
101- ### What is NOT stripped
119+ ### Disabling JSON5 stripping
120+
121+ Some cores accept comments natively, or you want to debug rendered output.
122+ Set ` "strip_json5": false ` in ` run.json ` :
123+
124+ ``` json5
125+ {
126+ " strip_json5" : false , // keep // and # comments in rendered config
127+ ...
128+ }
129+ ```
130+
131+ Default: ` true ` (strip — required by most proxy cores).
132+
133+ ### What is NOT stripped (when strip_json5: true )
102134
103135- ` // ` inside a string value: ` "url": "https://example.com/path//foo" ` → kept
104136- ` # ` inside a string value: ` "color": "#ff0000" ` → kept
105137- Single-quoted strings: ` 'value' ` → normalised to ` "value" `
106138
107139---
108140
109- ## Base template composition
141+ ## Template inheritance: ` {% extends %} ` + ` {% block %} `
110142
111- When a core has a ` templates/base/<role>.json.j2 ` file (e.g .
112- ` examples/xray/templates/base/client.json.j2 ` ), the runner:
143+ ** Recommended pattern ** — protocol templates extend the base template .
144+ Pongo2 handles composition natively; no Go-side merging.
113145
114- 1 . Renders the protocol template (e.g. ` vless-xhttp/client.json.j2 ` )
115- 2 . Renders the base template with the same resolved vars
116- 3 . Deep-merges them: ** base = defaults, protocol = overrides**
117-
118- This lets protocol templates be minimal (just ` outbounds ` ) while the base
119- provides the outer shell (` log ` , ` inbounds ` /SOCKS, ` routing ` ):
146+ ` examples/xray/templates/base/client.json.j2 ` :
120147
148+ ``` json5
149+ {
150+ " log" : {" loglevel" : " {{ LOG_LEVEL }}" },
151+ " inbounds" : [
152+ {" tag" : " socks-in" , " port" : {{ SOCKS_PORT }}, " protocol" : " socks" }
153+ ],
154+ " outbounds" : [
155+ {% block outbound % }
156+ {" protocol" : " freedom" , " tag" : " direct" }
157+ {% endblock % }
158+ ]
159+ }
121160```
122- examples/xray/templates/base/client.json.j2 ← log + socks inbound + routing
123- examples/xray/vless-xhttp/client.json.j2 ← just the vless outbound
124- ↓ deep merge
125- full xray client config
161+
162+ ` examples/xray/vless-xhttp/client.json.j2 ` :
163+
164+ ``` json5
165+ {% extends " ../templates/base/client.json.j2" % }
166+
167+ {% block outbound % }
168+ {
169+ " tag" : " vless-out" ,
170+ " protocol" : " vless" ,
171+ " streamSettings" : {
172+ {% include " ../templates/tls/client.tpl" % }
173+ " network" : " xhttp"
174+ }
175+ },
176+ {" protocol" : " freedom" , " tag" : " direct" }
177+ {% endblock % }
126178```
127179
180+ - ` {% extends %} ` must be the first statement in the file
181+ - ` {% include %} ` still works inside blocks
182+ - When ` {% extends %} ` is detected, the Go-side deep-merge fallback is skipped
183+
184+ ## Deep-merge fallback (without ` {% extends %} ` )
185+
186+ If a template does ** not** use ` {% extends %} ` , the runner checks for
187+ ` templates/base/<role>.json.j2 ` in ancestor directories and deep-merges:
188+ base = defaults, protocol = overrides.
189+
128190** Merge rules:**
129- - Object keys present only in base → kept
191+ - Object keys only in base → kept
130192- Object keys in both → protocol wins (recursive for nested objects)
131193- Arrays → protocol array replaces base array entirely
132194- ` null ` in protocol → removes the key from base
0 commit comments