|
9 | 9 | _path, kind, tags are always defined - no defensive checks needed. |
10 | 10 | ================================================================================ |
11 | 11 | #} |
12 | | -{# Template-wide variables using {% let %} for explicit scope #} |
| 12 | +{# Template-wide variables using {% let %} for explicit scope |
| 13 | + NOTE: `page` can be None in some render contexts (e.g., error shells, test fixtures). |
| 14 | + Kida 0.7.0 strict_undefined requires guarded access. #} |
13 | 15 | {% let params = params ?? {} %} |
14 | | -{% let _page_title = page.title %} |
15 | | -{% let _page_url = page._path %} |
| 16 | +{% let _page_title = page?.title ?? none %} |
| 17 | +{% let _page_url = page?._path ?? none %} |
16 | 18 |
|
17 | 19 | {# Navigation - cache function calls #} |
18 | 20 | {% let _current_lang = current_lang() %} |
|
23 | 25 | {% let _build_badge = site.build_badge %} |
24 | 26 | {% let _doc_app = site.document_application %} |
25 | 27 | {% let _link_previews = site.link_previews %} |
26 | | -{% let _per_page_json = 'json' in (config.output_formats.per_page ?? []) %} |
27 | | -{% let _per_page_md = 'markdown' in (config.output_formats.per_page ?? []) %} |
| 28 | +{% let _per_page_json = 'json' in ((config?.output_formats ?? {})?.per_page ?? []) %} |
| 29 | +{% let _per_page_md = 'markdown' in ((config?.output_formats ?? {})?.per_page ?? []) %} |
28 | 30 |
|
29 | 31 | {# Derived values #} |
30 | 32 | {% let _main_menu = get_menu_lang('main', _current_lang) %} |
31 | 33 | {% let _footer_menu = get_menu_lang('footer', _current_lang) %} |
32 | 34 |
|
33 | | -{% let _doc_app_nav = _doc_app?.navigation %} |
34 | | -{% let _view_transitions = _doc_app.enabled and _doc_app_nav?.view_transitions %} |
35 | | -{% let _transition_style = _doc_app_nav?.transition_style %} |
| 35 | +{% let _doc_app_nav = _doc_app?.navigation ?? {} %} |
| 36 | +{% let _view_transitions = (_doc_app?.enabled ?? false) and (_doc_app_nav?.view_transitions ?? false) %} |
| 37 | +{% let _transition_style = _doc_app_nav?.transition_style ?? none %} |
36 | 38 |
|
37 | 39 | {# Auto navigation if main menu is empty #} |
38 | 40 | {% let _auto_nav = get_auto_nav() if _main_menu | length == 0 else [] %} |
|
42 | 44 | ============================================================================= #} |
43 | 45 | {% def render_menu_item(item, is_mobile=false) %} |
44 | 46 | {% let has_children = item.children | length > 0 %} |
45 | | -{% let is_active = item.active or (_page_url == item.href) %} |
| 47 | +{% let is_active = (item?.active ?? false) or (_page_url == item?.href) %} |
46 | 48 | {% let is_trail = item.active_trail ?? false %} |
47 | 49 |
|
48 | 50 | <li |
|
70 | 72 | {% if has_children %} |
71 | 73 | <ul class="submenu"> |
72 | 74 | {% for child in item.children %} |
73 | | - <li class="{{ ['active' if child.active] | classes }}"> |
74 | | - <a href="{{ child.href | absolute_url }}"> |
75 | | - {% with child.icon as icon_name %} |
| 75 | + <li class="{{ ['active' if (child?.active ?? false)] | classes }}"> |
| 76 | + <a href="{{ (child?.href ?? '') | absolute_url }}"> |
| 77 | + {% with (child?.icon ?? none) as icon_name %} |
76 | 78 | {% if icon_name %}<span class="submenu-icon">{{ icon(icon_name, size=16) }}</span>{% end %} |
77 | 79 | {% end %} |
78 | | - <span class="submenu-text"><bdi>{{ child.name }}</bdi></span> |
| 80 | + <span class="submenu-text"><bdi>{{ child?.name ?? '' }}</bdi></span> |
79 | 81 | </a> |
80 | 82 | </li> |
81 | 83 | {% end %} |
|
110 | 112 | {# Title - captured for reuse in Open Graph and Twitter Card #} |
111 | 113 | {% capture page_title %} |
112 | 114 | {% block title %} |
113 | | - {% match page.title %} |
| 115 | + {% match page?.title ?? none %} |
114 | 116 | {% case title if title and _site_title %}{{ title }} - {{ _site_title }} |
115 | 117 | {% case title if title %}{{ title }} |
116 | 118 | {% case _ %}{{ _site_title }} |
|
126 | 128 | <meta name="description" content="{{ meta_desc | trim }}"> |
127 | 129 |
|
128 | 130 | {# Meta Keywords - use page.keywords or fallback to tags #} |
129 | | - {% if page.keywords %} |
| 131 | + {% if page?.keywords ?? none %} |
130 | 132 | <meta name="keywords" content="{{ page.keywords | join(', ') }}"> |
131 | | - {% elif page.tags %} |
| 133 | + {% elif page?.tags ?? none %} |
132 | 134 | <meta name="keywords" content="{{ page.tags | meta_keywords(10) }}"> |
133 | 135 | {% end %} |
134 | 136 |
|
135 | 137 | {# Robots directive for hidden/unlisted pages #} |
136 | | - {% if page.robots_meta and page.robots_meta != 'index, follow' %} |
| 138 | + {% if (page?.robots_meta ?? none) and page.robots_meta != 'index, follow' %} |
137 | 139 | <meta name="robots" content="{{ page.robots_meta }}"> |
138 | 140 | {% end %} |
139 | 141 |
|
140 | 142 | {# Content Signals — per-page AI directives (only when restricted) #} |
141 | | - {% if not page.in_ai_train or not page.in_ai_input %} |
142 | | - <meta name="content-signal:ai-train" content="{{ 'yes' if page.in_ai_train else 'no' }}"> |
143 | | - <meta name="content-signal:ai-input" content="{{ 'yes' if page.in_ai_input else 'no' }}"> |
| 143 | + {% let _ai_train = page?.in_ai_train ?? true %} |
| 144 | + {% let _ai_input = page?.in_ai_input ?? true %} |
| 145 | + {% if not _ai_train or not _ai_input %} |
| 146 | + <meta name="content-signal:ai-train" content="{{ 'yes' if _ai_train else 'no' }}"> |
| 147 | + <meta name="content-signal:ai-input" content="{{ 'yes' if _ai_input else 'no' }}"> |
144 | 148 | {% end %} |
145 | 149 |
|
146 | 150 | {# Open Graph / Facebook #} |
|
172 | 176 | <meta name="bengal:search_preload" content="{{ config.search_preload ?? 'smart' }}"> |
173 | 177 | <meta name="bengal:baseurl" content="{{ site.baseurl }}"> |
174 | 178 | <meta name="bengal:index_url" content="{{ '/index.json' | absolute_url }}"> |
175 | | - {% if bengal.capabilities.prebuilt_search %} |
| 179 | + {% if bengal?.capabilities?.prebuilt_search ?? false %} |
176 | 180 | <meta name="bengal:search_index_url" content="{{ '/search-index.json' | absolute_url }}"> |
177 | 181 | {% end %} |
178 | | - {% if page.version %} |
| 182 | + {% if page?.version ?? none %} |
179 | 183 | <meta name="bengal:version" content="{{ page.version }}"> |
180 | 184 | {% end %} |
181 | 185 |
|
|
185 | 189 | {% end %} |
186 | 190 |
|
187 | 191 | {# RSS Feed #} |
188 | | - {% with config.i18n as i18n %} |
| 192 | + {% with (config?.i18n ?? none) as i18n %} |
189 | 193 | {% let rss_href = '/rss.xml' %} |
190 | 194 | {% if i18n and i18n.strategy == 'prefix' %} |
191 | 195 | {% let default_lang = i18n.default_language ?? 'en' %} |
|
220 | 224 | <link rel="preconnect" href="https://d3js.org" crossorigin> |
221 | 225 |
|
222 | 226 | {# Fonts #} |
223 | | - {% if config.fonts %} |
| 227 | + {% if config?.fonts ?? none %} |
224 | 228 | <link rel="preload" href="{{ asset_url('fonts/outfit-700.woff2') }}" as="font" type="font/woff2" crossorigin> |
225 | 229 | <link rel="preload" href="{{ asset_url('fonts.css') }}" as="style" onload="this.onload=null;this.rel='stylesheet'"> |
226 | 230 | <noscript> |
|
292 | 296 |
|
293 | 297 | </head> |
294 | 298 |
|
295 | | -<body data-type="{{ page.type }}" data-variant="{{ page.variant or '' }}" |
296 | | - class="page-kind-{{ page.kind or 'page' }}{% if page.is_draft %} draft-page{% end %}{% if page.hidden %} hidden-page{% end %}{% if page.is_featured %} featured-content{% end %}"> |
| 299 | +<body data-type="{{ page?.type ?? '' }}" data-variant="{{ (page?.variant ?? '') or '' }}" |
| 300 | + class="page-kind-{{ (page?.kind ?? none) or 'page' }}{% if page?.is_draft ?? false %} draft-page{% end %}{% if page?.hidden ?? false %} hidden-page{% end %}{% if page?.is_featured ?? false %} featured-content{% end %}"> |
297 | 301 |
|
298 | 302 | {% if 'accessibility.skip_link' in (theme.features ?? []) %} |
299 | 303 | <a href="#main-content" class="skip-link">Skip to main content</a> |
|
328 | 332 | </li> |
329 | 333 | {% end %} |
330 | 334 | {% end %} |
331 | | - {% with site.params.repo_url as repo_url %} |
| 335 | + {% with (site?.params?.repo_url ?? none) as repo_url %} |
332 | 336 | {% if repo_url and not (site._dev_menu_metadata?.github_bundled ?? false) %} |
333 | 337 | <li><a href="{{ repo_url }}" target="_blank" rel="noopener" aria-label="GitHub">{{ |
334 | 338 | icon('github-logo', size=16) }} <span>GitHub</span></a></li> |
|
340 | 344 | {% end %} |
341 | 345 |
|
342 | 346 | <div class="header-actions hidden-mobile"> |
343 | | - {% with config.search.ui as ui %} |
344 | | - {% if ui.modal ?? false %} |
| 347 | + {% with (config?.search?.ui ?? none) as ui %} |
| 348 | + {% if ui and (ui?.modal ?? false) %} |
345 | 349 | <button type="button" class="nav-search-trigger" id="nav-search-trigger" title="Search (⌘K)">{{ |
346 | 350 | icon('magnifying-glass', size=16) }} <span>Search</span> <kbd |
347 | 351 | class="nav-search-shortcut">⌘K</kbd></button> |
|
366 | 370 | {# Search Modal Block — placed after <main> so content starts early in DOM #} |
367 | 371 | {% block site_search_modal %} |
368 | 372 | {% cache 'base-search-modal-' ~ _current_lang ~ '-' ~ (bengal.build.timestamp ?? '') %} |
369 | | - {% with config.search.ui as ui %} |
370 | | - {% if ui.modal ?? false %} |
| 373 | + {% with (config?.search?.ui ?? none) as ui %} |
| 374 | + {% if ui and (ui?.modal ?? false) %} |
371 | 375 | <dialog id="search-modal" class="search-modal" |
372 | 376 | aria-label="{{ t('search.aria_label', default='Search documentation') }}"> |
373 | 377 | <div class="search-modal__backdrop" data-close-modal></div> |
|
462 | 466 | <a href="{{ item.href | absolute_url }}">{{ item.name }}</a> |
463 | 467 | </li>{% end %} |
464 | 468 | {% end %} |
465 | | - {% with site.params.repo_url as repo_url %} |
| 469 | + {% with (site?.params?.repo_url ?? none) as repo_url %} |
466 | 470 | {% if repo_url and not (site._dev_menu_metadata?.github_bundled ?? false) %} |
467 | 471 | <li><a href="{{ repo_url }}" target="_blank" rel="noopener">{{ icon('github-logo', size=16) }} |
468 | 472 | GitHub</a></li> |
|
589 | 593 | {% end %}{# /cache base-speculation #} |
590 | 594 |
|
591 | 595 | {# Bootstrap metadata — moved after content for faster content-start position #} |
592 | | - {% if config.expose_metadata_json %} |
| 596 | + {% if config?.expose_metadata_json ?? false %} |
593 | 597 | <script id="bengal-bootstrap" type="application/json">{{ bengal | tojson }}</script> |
594 | 598 | <script> |
595 | 599 | (function () { |
|
0 commit comments