PageTemplatesPage.vala 19 KB

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