PageTemplatesPage.vala 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. using Spry;
  2. using Inversion;
  3. /**
  4. * PageTemplatesPage - Documentation for Page Templates
  5. *
  6. * This page explains how PageTemplates work, how to create them,
  7. * and how they wrap PageComponents to provide site-wide layouts.
  8. */
  9. public class PageTemplatesPage : PageComponent {
  10. public const string ROUTE = "/page-components/templates";
  11. private ComponentFactory factory = inject<ComponentFactory>();
  12. public override string markup { get {
  13. return """
  14. <div class="doc-content">
  15. <div class="doc-hero">
  16. <h1>Page Templates</h1>
  17. <p class="doc-subtitle">Create reusable layouts that wrap your page components</p>
  18. </div>
  19. <section class="doc-section">
  20. <h2>What are Page Templates?</h2>
  21. <p>
  22. A <code>PageTemplate</code> is a special component that provides the outer HTML
  23. structure for your pages. Templates define the <code><html></code>,
  24. <code><head></code>, <code><body></code>, and common elements like
  25. navigation headers and footers.
  26. </p>
  27. <p>
  28. The key feature of a PageTemplate is the <code>&lt;spry-template-outlet/&gt;</code>
  29. element, which marks where page content will be inserted. When a PageComponent
  30. renders, it automatically gets nested inside matching templates.
  31. </p>
  32. </section>
  33. <section class="doc-section">
  34. <h2>The Template Outlet</h2>
  35. <p>
  36. The <code>&lt;spry-template-outlet/&gt;</code> element is the placeholder where
  37. page content gets inserted. Every PageTemplate must include at least one outlet.
  38. </p>
  39. <spry-component name="CodeBlockComponent" sid="outlet-example"/>
  40. <div class="info-box">
  41. <p>
  42. <strong>💡 How it works:</strong> When rendering, Spry finds all templates
  43. matching the current route, renders them in order of specificity, and nests
  44. each one's content into the previous template's outlet.
  45. </p>
  46. </div>
  47. </section>
  48. <section class="doc-section">
  49. <h2>Creating a MainTemplate</h2>
  50. <p>
  51. Here's a complete example of a site-wide template that provides the HTML
  52. document structure, head elements, navigation, and footer:
  53. </p>
  54. <spry-component name="CodeBlockComponent" sid="main-template"/>
  55. </section>
  56. <section class="doc-section">
  57. <h2>Template Registration</h2>
  58. <p>
  59. Templates are registered with a <strong>prefix</strong> that determines which
  60. routes they apply to. The prefix is passed via metadata during registration:
  61. </p>
  62. <spry-component name="CodeBlockComponent" sid="template-registration"/>
  63. <p>
  64. In this example, the <code>TemplateRoutePrefix("")</code> with an empty string
  65. matches <strong>all routes</strong>, making it the default site-wide template.
  66. </p>
  67. </section>
  68. <section class="doc-section">
  69. <h2>Route Prefix Matching</h2>
  70. <p>
  71. Templates can target specific sections of your site using route prefixes.
  72. The prefix determines which routes the template will wrap:
  73. </p>
  74. <table class="doc-table">
  75. <thead>
  76. <tr>
  77. <th>Prefix</th>
  78. <th>Matches Routes</th>
  79. <th>Use Case</th>
  80. </tr>
  81. </thead>
  82. <tbody>
  83. <tr>
  84. <td><code>""</code> (empty)</td>
  85. <td>All routes</td>
  86. <td>Site-wide default template</td>
  87. </tr>
  88. <tr>
  89. <td><code>"/admin"</code></td>
  90. <td>/admin/*, /admin/settings, etc.</td>
  91. <td>Admin section layout</td>
  92. </tr>
  93. <tr>
  94. <td><code>"/docs"</code></td>
  95. <td>/docs/*, /docs/getting-started, etc.</td>
  96. <td>Documentation section</td>
  97. </tr>
  98. <tr>
  99. <td><code>"/api"</code></td>
  100. <td>/api/* routes</td>
  101. <td>API documentation or explorer</td>
  102. </tr>
  103. </tbody>
  104. </table>
  105. <h3>How Matching Works</h3>
  106. <p>
  107. The <code>TemplateRoutePrefix.matches_route()</code> method compares the template's
  108. prefix segments against the route segments. A template matches if its prefix
  109. segments are a prefix of the route segments.
  110. </p>
  111. <spry-component name="CodeBlockComponent" sid="matching-logic"/>
  112. </section>
  113. <section class="doc-section">
  114. <h2>Multiple Templates</h2>
  115. <p>
  116. You can have multiple templates for different sections of your site.
  117. Templates are sorted by <strong>rank</strong> (prefix depth) and applied
  118. from lowest to highest rank:
  119. </p>
  120. <spry-component name="CodeBlockComponent" sid="multiple-templates"/>
  121. <h3>Template Nesting Order</h3>
  122. <p>
  123. For a route like <code>/admin/users</code>, templates would be applied in order:
  124. </p>
  125. <ol class="doc-list">
  126. <li><strong>MainTemplate</strong> (prefix: "") - rank 0</li>
  127. <li><strong>AdminTemplate</strong> (prefix: "/admin") - rank 1</li>
  128. <li><strong>PageComponent</strong> - The actual page content</li>
  129. </ol>
  130. <p>
  131. Each template's outlet receives the content from the next item in the chain,
  132. creating nested layouts.
  133. </p>
  134. </section>
  135. <section class="doc-section">
  136. <h2>Section-Specific Template Example</h2>
  137. <p>
  138. Here's an example of a template specifically for the admin section that
  139. adds an admin sidebar:
  140. </p>
  141. <spry-component name="CodeBlockComponent" sid="admin-template"/>
  142. </section>
  143. <section class="doc-section">
  144. <h2>Head Content Merging</h2>
  145. <p>
  146. When templates wrap pages, the <code><head></code> elements are automatically
  147. merged. If a PageComponent or nested template has <code><head></code> content,
  148. those elements are appended to the outer template's head.
  149. </p>
  150. <p>
  151. This allows pages to add their own stylesheets, scripts, or meta tags while
  152. still benefiting from the template's common head elements.
  153. </p>
  154. <div class="warning-box">
  155. <p>
  156. <strong>⚠️ Note:</strong> Head merging only works when templates render
  157. actual <code><head></code> elements. Make sure your templates include
  158. a proper HTML structure with head and body sections.
  159. </p>
  160. </div>
  161. </section>
  162. <section class="doc-section">
  163. <h2>Template vs Component</h2>
  164. <table class="doc-table">
  165. <thead>
  166. <tr>
  167. <th>Feature</th>
  168. <th>PageTemplate</th>
  169. <th>Component</th>
  170. </tr>
  171. </thead>
  172. <tbody>
  173. <tr>
  174. <td>Base class</td>
  175. <td><code>Component</code></td>
  176. <td><code>Component</code></td>
  177. </tr>
  178. <tr>
  179. <td>Has markup</td>
  180. <td>✓ Yes</td>
  181. <td>✓ Yes</td>
  182. </tr>
  183. <tr>
  184. <td>Contains outlet</td>
  185. <td>✓ Required</td>
  186. <td>✗ Optional</td>
  187. </tr>
  188. <tr>
  189. <td>Route matching</td>
  190. <td>By prefix</td>
  191. <td>N/A</td>
  192. </tr>
  193. <tr>
  194. <td>Wraps pages</td>
  195. <td>✓ Yes</td>
  196. <td>✗ No</td>
  197. </tr>
  198. </tbody>
  199. </table>
  200. </section>
  201. <section class="doc-section">
  202. <h2>Best Practices</h2>
  203. <ul class="doc-list">
  204. <li><strong>Keep templates focused:</strong> Each template should handle one level of layout (site-wide, section-specific)</li>
  205. <li><strong>Use semantic HTML:</strong> Include proper <code><header></code>, <code><main></code>, <code><footer></code> elements</li>
  206. <li><strong>Include common resources:</strong> Add shared stylesheets and scripts in your main template</li>
  207. <li><strong>Plan your prefix hierarchy:</strong> Design your URL structure to work with template prefixes</li>
  208. <li><strong>Don't duplicate content:</strong> Let templates handle repeated elements like navigation</li>
  209. </ul>
  210. </section>
  211. <section class="doc-section">
  212. <h2>Next Steps</h2>
  213. <div class="nav-cards">
  214. <a href="/page-components/overview" class="nav-card">
  215. <h3>← Page Components</h3>
  216. <p>Learn about PageComponent basics</p>
  217. </a>
  218. <a href="/components/outlets" class="nav-card">
  219. <h3>Component Outlets →</h3>
  220. <p>Use outlets for nested components</p>
  221. </a>
  222. </div>
  223. </section>
  224. </div>
  225. """;
  226. }}
  227. public override async void prepare() throws Error {
  228. // Outlet example
  229. var outlet_example = get_component_child<CodeBlockComponent>("outlet-example");
  230. outlet_example.language = "xml";
  231. outlet_example.code = "<!DOCTYPE html>\n" +
  232. "<html lang=\"en\">\n" +
  233. "<head>\n" +
  234. " <title>My Site</title>\n" +
  235. " <link rel=\"stylesheet\" href=\"/styles/main.css\">\n" +
  236. "</head>\n" +
  237. "<body>\n" +
  238. " <header>\n" +
  239. " <nav><!-- Site navigation --></nav>\n" +
  240. " </header>\n" +
  241. " \n" +
  242. " <main>\n" +
  243. " <!-- Page content is inserted here -->\n" +
  244. " <spry-template-outlet />\n" +
  245. " </main>\n" +
  246. " \n" +
  247. " <footer>\n" +
  248. " <p>&copy; 2024 My Site</p>\n" +
  249. " </footer>\n" +
  250. "</body>\n" +
  251. "</html>";
  252. // Main template example
  253. var main_template = get_component_child<CodeBlockComponent>("main-template");
  254. main_template.language = "vala";
  255. main_template.code = "using Spry;\n\n" +
  256. "/**\n" +
  257. " * MainTemplate - Site-wide layout template\n" +
  258. " * \n" +
  259. " * Wraps all pages with common HTML structure,\n" +
  260. " * navigation, and footer.\n" +
  261. " */\n" +
  262. "public class MainTemplate : PageTemplate {\n" +
  263. " \n" +
  264. " public override string markup { get {\n" +
  265. " return \"\"\"\n" +
  266. " <!DOCTYPE html>\n" +
  267. " <html lang=\"en\">\n" +
  268. " <head>\n" +
  269. " <meta charset=\"UTF-8\">\n" +
  270. " <meta name=\"viewport\" \n" +
  271. " content=\"width=device-width, initial-scale=1.0\">\n" +
  272. " <title>My Spry Application</title>\n" +
  273. " <link rel=\"stylesheet\" href=\"/styles/main.css\">\n" +
  274. " <script spry-res=\"htmx.js\"></script>\n" +
  275. " </head>\n" +
  276. " <body>\n" +
  277. " <header class=\"site-header\">\n" +
  278. " <nav>\n" +
  279. " <a href=\"/\" class=\"logo\">My App</a>\n" +
  280. " <ul>\n" +
  281. " <li><a href=\"/docs\">Docs</a></li>\n" +
  282. " <li><a href=\"/about\">About</a></li>\n" +
  283. " </ul>\n" +
  284. " </nav>\n" +
  285. " </header>\n" +
  286. " \n" +
  287. " <main>\n" +
  288. " <spry-template-outlet />\n" +
  289. " </main>\n" +
  290. " \n" +
  291. " <footer class=\"site-footer\">\n" +
  292. " <p>&copy; 2024 My App. Built with Spry.</p>\n" +
  293. " </footer>\n" +
  294. " </body>\n" +
  295. " </html>\n" +
  296. " \"\"\";\n" +
  297. " }}\n" +
  298. "}";
  299. // Template registration
  300. var template_reg = get_component_child<CodeBlockComponent>("template-registration");
  301. template_reg.language = "vala";
  302. template_reg.code = "// In Main.vala:\n\n" +
  303. "var spry_cfg = application.configure_with<SpryConfigurator>();\n\n" +
  304. "// Register template with empty prefix (matches all routes)\n" +
  305. "spry_cfg.add_template<MainTemplate>(\"\");\n\n" +
  306. "// The TemplateRoutePrefix is created internally from the string\n" +
  307. "// and used for route matching";
  308. // Matching logic
  309. var matching_logic = get_component_child<CodeBlockComponent>("matching-logic");
  310. matching_logic.language = "vala";
  311. matching_logic.code = "// TemplateRoutePrefix matching logic (simplified)\n\n" +
  312. "public class TemplateRoutePrefix : Object {\n" +
  313. " public uint rank { get; private set; }\n" +
  314. " public string prefix { get; private set; }\n" +
  315. " \n" +
  316. " public TemplateRoutePrefix(string prefix) {\n" +
  317. " this.prefix = prefix;\n" +
  318. " // Rank is the number of path segments\n" +
  319. " rank = prefix.split(\"/\").length - 1;\n" +
  320. " }\n" +
  321. " \n" +
  322. " public bool matches_route(RouteContext context) {\n" +
  323. " // Returns true if prefix segments match\n" +
  324. " // the beginning of the route segments\n" +
  325. " return context.matched_route.route_segments\n" +
  326. " .starts_with(this.prefix_segments);\n" +
  327. " }\n" +
  328. "}\n\n" +
  329. "// Example: prefix \"/admin\" matches:\n" +
  330. "// /admin ✓ (exact match)\n" +
  331. "// /admin/users ✓ (prefix match)\n" +
  332. "// /admin/settings ✓ (prefix match)\n" +
  333. "// /user/admin ✗ (not a prefix match)";
  334. // Multiple templates
  335. var multiple_templates = get_component_child<CodeBlockComponent>("multiple-templates");
  336. multiple_templates.language = "vala";
  337. multiple_templates.code = "// In Main.vala:\n\n" +
  338. "var spry_cfg = application.configure_with<SpryConfigurator>();\n\n" +
  339. "// Site-wide template (rank 0, matches everything)\n" +
  340. "spry_cfg.add_template<MainTemplate>(\"\");\n\n" +
  341. "// Admin section template (rank 1, matches /admin/*)\n" +
  342. "spry_cfg.add_template<AdminTemplate>(\"/admin\");\n\n" +
  343. "// Documentation template (rank 1, matches /docs/*)\n" +
  344. "spry_cfg.add_template<DocsTemplate>(\"/docs\");\n\n" +
  345. "// API docs template (rank 2, matches /docs/api/*)\n" +
  346. "spry_cfg.add_template<ApiDocsTemplate>(\"/docs/api\");";
  347. // Admin template example
  348. var admin_template = get_component_child<CodeBlockComponent>("admin-template");
  349. admin_template.language = "vala";
  350. admin_template.code = "using Spry;\n\n" +
  351. "/**\n" +
  352. " * AdminTemplate - Layout for admin section\n" +
  353. " * \n" +
  354. " * Adds an admin sidebar to all /admin/* pages.\n" +
  355. " */\n" +
  356. "public class AdminTemplate : PageTemplate {\n" +
  357. " \n" +
  358. " public override string markup { get {\n" +
  359. " return \"\"\"\n" +
  360. " <div class=\"admin-layout\">\n" +
  361. " <aside class=\"admin-sidebar\">\n" +
  362. " <h3>Admin</h3>\n" +
  363. " <nav>\n" +
  364. " <a href=\"/admin\">Dashboard</a>\n" +
  365. " <a href=\"/admin/users\">Users</a>\n" +
  366. " <a href=\"/admin/settings\">Settings</a>\n" +
  367. " </nav>\n" +
  368. " </aside>\n" +
  369. " \n" +
  370. " <div class=\"admin-content\">\n" +
  371. " <!-- Page content inserted here -->\n" +
  372. " <spry-template-outlet />\n" +
  373. " </div>\n" +
  374. " </div>\n" +
  375. " \"\"\";\n" +
  376. " }}\n" +
  377. "}\n\n" +
  378. "// Register with /admin prefix\n" +
  379. "// spry_cfg.add_template<AdminTemplate>(\"/admin\");";
  380. }
  381. }