brut 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (521) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/brut-css/package-lock.json +2 -2
  5. data/brut-css/package.json +1 -1
  6. data/brut-js/package-lock.json +2 -2
  7. data/brut-js/package.json +1 -1
  8. data/brut-js/specs/Toast.spec.js +34 -0
  9. data/brut-js/src/I18nTranslation.js +3 -0
  10. data/brut-js/src/Message.js +9 -3
  11. data/brut-js/src/RichString.js +4 -1
  12. data/brut-js/src/Toast.js +102 -0
  13. data/brut-js/src/index.js +3 -0
  14. data/brutrb.com/brut-js.md +1 -0
  15. data/docs/404.html +3 -3
  16. data/docs/adrs.html +7 -7
  17. data/docs/ai.html +7 -7
  18. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  19. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  20. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  21. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  22. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  23. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  24. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  25. data/docs/api/Brut/BackEnd.html +1 -1
  26. data/docs/api/Brut/CLI/App.html +1 -1
  27. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  28. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  29. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  30. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  31. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  32. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  33. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  34. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  35. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  36. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  37. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  38. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  39. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  40. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  41. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  42. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  43. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  45. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  46. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  47. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  48. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +1 -1
  50. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  51. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  52. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  53. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  54. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  55. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  56. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  57. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  58. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  59. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  60. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  61. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  62. data/docs/api/Brut/CLI/Apps.html +1 -1
  63. data/docs/api/Brut/CLI/Command.html +1 -1
  64. data/docs/api/Brut/CLI/Error.html +1 -1
  65. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  66. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  67. data/docs/api/Brut/CLI/Executor.html +1 -1
  68. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  69. data/docs/api/Brut/CLI/Options.html +1 -1
  70. data/docs/api/Brut/CLI/Output.html +1 -1
  71. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  72. data/docs/api/Brut/CLI.html +1 -1
  73. data/docs/api/Brut/FactoryBot.html +1 -1
  74. data/docs/api/Brut/Framework/App.html +1 -1
  75. data/docs/api/Brut/Framework/Config.html +1 -1
  76. data/docs/api/Brut/Framework/Container.html +1 -1
  77. data/docs/api/Brut/Framework/Error.html +1 -1
  78. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  79. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  80. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  81. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  82. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  83. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  84. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  85. data/docs/api/Brut/Framework/Errors.html +1 -1
  86. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  87. data/docs/api/Brut/Framework/MCP.html +1 -1
  88. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  89. data/docs/api/Brut/Framework.html +1 -1
  90. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  91. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  92. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  93. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  94. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  95. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  96. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  97. data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +1 -1
  98. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  99. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  100. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  101. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  102. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  103. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  104. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  105. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +37 -18
  106. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  107. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  108. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  109. data/docs/api/Brut/FrontEnd/CsrfProtector.html +1 -1
  110. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  111. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  112. data/docs/api/Brut/FrontEnd/Form.html +1 -1
  113. data/docs/api/Brut/FrontEnd/Forms/Button.html +1 -1
  114. data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +1 -1
  115. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  116. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  117. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  118. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
  122. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  123. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
  124. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  125. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  127. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  128. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  129. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  130. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  136. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  137. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  138. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  139. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Layout.html +171 -3
  141. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  150. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  151. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  152. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  153. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  154. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  155. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  156. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  157. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  158. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  169. data/docs/api/Brut/FrontEnd.html +1 -1
  170. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  171. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  172. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  173. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  174. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  175. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  176. data/docs/api/Brut/I18n.html +1 -1
  177. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  178. data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +1 -1
  179. data/docs/api/Brut/Instrumentation/Methods.html +1 -1
  180. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  181. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  182. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  183. data/docs/api/Brut/Instrumentation.html +1 -1
  184. data/docs/api/Brut/RubocopConfig.html +1 -1
  185. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  186. data/docs/api/Brut/SinatraHelpers.html +1 -1
  187. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  188. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  189. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  190. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  191. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  192. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  193. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  194. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  195. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  196. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  197. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  198. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  199. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  200. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  201. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  202. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  203. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  204. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  205. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  206. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  207. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  208. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  209. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  210. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  211. data/docs/api/Brut/SpecSupport.html +1 -1
  212. data/docs/api/Brut.html +1 -1
  213. data/docs/api/Clock.html +1 -1
  214. data/docs/api/ModuleName.html +1 -1
  215. data/docs/api/RichString.html +1 -1
  216. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  217. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  218. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  219. data/docs/api/Sequel/Extensions.html +1 -1
  220. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  221. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  222. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  223. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  224. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  225. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  226. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  227. data/docs/api/Sequel/Plugins.html +1 -1
  228. data/docs/api/Sequel.html +1 -1
  229. data/docs/api/_index.html +1 -1
  230. data/docs/api/file.README.html +1 -1
  231. data/docs/api/index.html +1 -1
  232. data/docs/api/method_list.html +157 -141
  233. data/docs/api/top-level-namespace.html +1 -1
  234. data/docs/assets/adrs.md.YglbWtQe.js +1 -0
  235. data/docs/assets/adrs.md.YglbWtQe.lean.js +1 -0
  236. data/docs/assets/ai.md.ChLnvDAX.js +1 -0
  237. data/docs/assets/ai.md.ChLnvDAX.lean.js +1 -0
  238. data/docs/assets/{app.BDtsVxyd.js → app.B0X8upRm.js} +1 -1
  239. data/docs/assets/{assets.md.7C3HWkga.js → assets.md.BEF6Oz6K.js} +2 -2
  240. data/docs/assets/assets.md.BEF6Oz6K.lean.js +1 -0
  241. data/docs/assets/{brut-js.md.B4GYxQVw.js → brut-js.md.CbJAe2Ky.js} +2 -2
  242. data/docs/assets/brut-js.md.CbJAe2Ky.lean.js +1 -0
  243. data/docs/assets/business-logic.md.DbuaOYGU.js +1 -0
  244. data/docs/assets/business-logic.md.DbuaOYGU.lean.js +1 -0
  245. data/docs/assets/chunks/@localSearchIndexroot.C0s1k0UQ.js +1 -0
  246. data/docs/assets/chunks/VPLocalSearchBox.jLmhant1.js +8 -0
  247. data/docs/assets/chunks/framework.C4nOkCZI.js +18 -0
  248. data/docs/assets/chunks/{theme.DZKmijwi.js → theme.CtVUdCdt.js} +2 -2
  249. data/docs/assets/{cli.md.CjsktgFz.js → cli.md.DDMar_51.js} +2 -2
  250. data/docs/assets/cli.md.DDMar_51.lean.js +1 -0
  251. data/docs/assets/{components.md.rMhQ0WdZ.js → components.md.C6nWgDP0.js} +5 -5
  252. data/docs/assets/components.md.C6nWgDP0.lean.js +1 -0
  253. data/docs/assets/{configuration.md.BK42Yjp_.js → configuration.md.CpbYHWPb.js} +2 -2
  254. data/docs/assets/configuration.md.CpbYHWPb.lean.js +1 -0
  255. data/docs/assets/{css.md.CltvJqAa.js → css.md.K5rOCOQY.js} +2 -2
  256. data/docs/assets/css.md.K5rOCOQY.lean.js +1 -0
  257. data/docs/assets/{custom-element-tests.md.B_rbta32.js → custom-element-tests.md.DiLe-eFw.js} +2 -2
  258. data/docs/assets/custom-element-tests.md.DiLe-eFw.lean.js +1 -0
  259. data/docs/assets/{database-access.md.gnluu54N.js → database-access.md.Dc8l2Plf.js} +2 -2
  260. data/docs/assets/database-access.md.Dc8l2Plf.lean.js +1 -0
  261. data/docs/assets/{database-schema.md.LpmBPVEU.js → database-schema.md.BJ_JhXmO.js} +2 -2
  262. data/docs/assets/database-schema.md.BJ_JhXmO.lean.js +1 -0
  263. data/docs/assets/{deployment.md.BLseERGV.js → deployment.md.C1u5ep0g.js} +2 -2
  264. data/docs/assets/deployment.md.C1u5ep0g.lean.js +1 -0
  265. data/docs/assets/{dev-environment.md.DRH2D2-O.js → dev-environment.md.B1S9p5ZK.js} +2 -2
  266. data/docs/assets/{dev-environment.md.DRH2D2-O.lean.js → dev-environment.md.B1S9p5ZK.lean.js} +1 -1
  267. data/docs/assets/{dir-structure.md.CWir1pic.js → dir-structure.md.D1T2kGwj.js} +2 -2
  268. data/docs/assets/dir-structure.md.D1T2kGwj.lean.js +1 -0
  269. data/docs/assets/doc-conventions.md.CDnWaEFg.js +1 -0
  270. data/docs/assets/doc-conventions.md.CDnWaEFg.lean.js +1 -0
  271. data/docs/assets/{end-to-end-tests.md.DzqRpZ43.js → end-to-end-tests.md.BJJdNDYL.js} +2 -2
  272. data/docs/assets/end-to-end-tests.md.BJJdNDYL.lean.js +1 -0
  273. data/docs/assets/{features.md.DPFXsy0z.js → features.md.BDWxnyNO.js} +2 -2
  274. data/docs/assets/features.md.BDWxnyNO.lean.js +1 -0
  275. data/docs/assets/{flash-and-session.md.nPvUpnUx.js → flash-and-session.md.CUsMxoNl.js} +2 -2
  276. data/docs/assets/flash-and-session.md.CUsMxoNl.lean.js +1 -0
  277. data/docs/assets/{form-constraints.md.KTv5cdR4.js → form-constraints.md.KlfXSKm2.js} +2 -2
  278. data/docs/assets/form-constraints.md.KlfXSKm2.lean.js +1 -0
  279. data/docs/assets/{forms.md.v9qIbmUM.js → forms.md.Bii91k3E.js} +3 -3
  280. data/docs/assets/forms.md.Bii91k3E.lean.js +1 -0
  281. data/docs/assets/{getting-started.md.DTOl4c2g.js → getting-started.md.ChAvueK7.js} +4 -4
  282. data/docs/assets/getting-started.md.ChAvueK7.lean.js +1 -0
  283. data/docs/assets/{handlers.md.h84MMB1R.js → handlers.md.C5tUwmmo.js} +2 -2
  284. data/docs/assets/handlers.md.C5tUwmmo.lean.js +1 -0
  285. data/docs/assets/{hooks.md.Jmb5VOLA.js → hooks.md.CoiYCKRc.js} +2 -2
  286. data/docs/assets/hooks.md.CoiYCKRc.lean.js +1 -0
  287. data/docs/assets/{i18n.md.BAm9t9JJ.js → i18n.md.DxkCKhUw.js} +2 -2
  288. data/docs/assets/i18n.md.DxkCKhUw.lean.js +1 -0
  289. data/docs/assets/{index.md.Bn9e0sRJ.js → index.md.DnphWyQd.js} +1 -1
  290. data/docs/assets/{index.md.Bn9e0sRJ.lean.js → index.md.DnphWyQd.lean.js} +1 -1
  291. data/docs/assets/{instrumentation.md._lNSriEZ.js → instrumentation.md.BcxjC4jd.js} +2 -2
  292. data/docs/assets/instrumentation.md.BcxjC4jd.lean.js +1 -0
  293. data/docs/assets/{javascript.md.DzrMxUmI.js → javascript.md.D6fxhaQb.js} +2 -2
  294. data/docs/assets/javascript.md.D6fxhaQb.lean.js +1 -0
  295. data/docs/assets/jobs.md.Bc7Y1YpK.js +1 -0
  296. data/docs/assets/jobs.md.Bc7Y1YpK.lean.js +1 -0
  297. data/docs/assets/{keyword-injection.md.95Zgh2eN.js → keyword-injection.md.CqLnnzIz.js} +2 -2
  298. data/docs/assets/keyword-injection.md.CqLnnzIz.lean.js +1 -0
  299. data/docs/assets/layouts.md.HEbeK7Jr.js +68 -0
  300. data/docs/assets/layouts.md.HEbeK7Jr.lean.js +1 -0
  301. data/docs/assets/lsp.md.bE9dW8n9.js +1 -0
  302. data/docs/assets/lsp.md.bE9dW8n9.lean.js +1 -0
  303. data/docs/assets/{markdown-examples.md.CCFEQO44.js → markdown-examples.md.BPmtHlc-.js} +2 -2
  304. data/docs/assets/markdown-examples.md.BPmtHlc-.lean.js +1 -0
  305. data/docs/assets/{middleware.md.Czz_UlJN.js → middleware.md.BhOIsg59.js} +2 -2
  306. data/docs/assets/middleware.md.BhOIsg59.lean.js +1 -0
  307. data/docs/assets/overview.md.BpWAgPFH.js +1 -0
  308. data/docs/assets/overview.md.BpWAgPFH.lean.js +1 -0
  309. data/docs/assets/{pages.md.B7Hc-i6H.js → pages.md.B3sQXpEd.js} +2 -2
  310. data/docs/assets/pages.md.B3sQXpEd.lean.js +1 -0
  311. data/docs/assets/{recipes_alternate-layouts.md.BwEytl59.js → recipes_alternate-layouts.md.C1QzVkA7.js} +2 -2
  312. data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.lean.js +1 -0
  313. data/docs/assets/{recipes_authentication.md.nwO6F7Ou.js → recipes_authentication.md.CyvoIW82.js} +2 -2
  314. data/docs/assets/recipes_authentication.md.CyvoIW82.lean.js +1 -0
  315. data/docs/assets/{recipes_custom-flash.md.CrQbI5eH.js → recipes_custom-flash.md.6gFqf2uL.js} +2 -2
  316. data/docs/assets/recipes_custom-flash.md.6gFqf2uL.lean.js +1 -0
  317. data/docs/assets/{recipes_form-errors.md.Bv5RCKqH.js → recipes_form-errors.md.B5ptSzMO.js} +2 -2
  318. data/docs/assets/recipes_form-errors.md.B5ptSzMO.lean.js +1 -0
  319. data/docs/assets/{recipes_indexed-forms.md.CstYyOSo.js → recipes_indexed-forms.md.BYYQGW2C.js} +2 -2
  320. data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.lean.js +1 -0
  321. data/docs/assets/{recipes_migrations.md.CTcnWDJF.js → recipes_migrations.md.Cid7-3cu.js} +2 -2
  322. data/docs/assets/recipes_migrations.md.Cid7-3cu.lean.js +1 -0
  323. data/docs/assets/{recipes_text-field-component.md.H4wLAK0Z.js → recipes_text-field-component.md.VhOsCtKI.js} +2 -2
  324. data/docs/assets/recipes_text-field-component.md.VhOsCtKI.lean.js +1 -0
  325. data/docs/assets/roadmap.md.CJsbUmK_.js +1 -0
  326. data/docs/assets/roadmap.md.CJsbUmK_.lean.js +1 -0
  327. data/docs/assets/{routes.md.BD6y2i-f.js → routes.md.C1dgIBtD.js} +2 -2
  328. data/docs/assets/routes.md.C1dgIBtD.lean.js +1 -0
  329. data/docs/assets/security.md.Jn4SY1uK.js +1 -0
  330. data/docs/assets/security.md.Jn4SY1uK.lean.js +1 -0
  331. data/docs/assets/{seed-data.md.BvFZlqIk.js → seed-data.md.UZW0WxYN.js} +2 -2
  332. data/docs/assets/seed-data.md.UZW0WxYN.lean.js +1 -0
  333. data/docs/assets/space-time-continuum.md.D9rYGDFH.js +1 -0
  334. data/docs/assets/space-time-continuum.md.D9rYGDFH.lean.js +1 -0
  335. data/docs/assets/{tutorial.md.BM40jnoq.js → tutorial.md.BX6f6l00.js} +2 -2
  336. data/docs/assets/tutorial.md.BX6f6l00.lean.js +1 -0
  337. data/docs/assets/{tutorials_01-intro.md.B4sUBY3X.js → tutorials_01-intro.md.CzZ3kpF_.js} +2 -2
  338. data/docs/assets/{tutorials_01-intro.md.B4sUBY3X.lean.js → tutorials_01-intro.md.CzZ3kpF_.lean.js} +1 -1
  339. data/docs/assets/{tutorials_02-dialog.md.CPNK1SC_.js → tutorials_02-dialog.md.De6iTsWX.js} +2 -2
  340. data/docs/assets/{tutorials_02-dialog.md.CPNK1SC_.lean.js → tutorials_02-dialog.md.De6iTsWX.lean.js} +1 -1
  341. data/docs/assets/{unit-tests.md.DUGrnLj5.js → unit-tests.md.vDsdBbO_.js} +2 -2
  342. data/docs/assets/unit-tests.md.vDsdBbO_.lean.js +1 -0
  343. data/docs/assets/why.md.4WpxdrQ2.js +1 -0
  344. data/docs/assets/why.md.4WpxdrQ2.lean.js +1 -0
  345. data/docs/assets.html +7 -7
  346. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  347. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  348. data/docs/brut-js/api/Autosubmit.html +1 -1
  349. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  350. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  351. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  352. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  353. data/docs/brut-js/api/BufferedLogger.html +1 -1
  354. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  355. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  356. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  357. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  358. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  359. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  360. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  361. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  362. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  363. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  364. data/docs/brut-js/api/Form.html +1 -1
  365. data/docs/brut-js/api/Form.js.html +1 -1
  366. data/docs/brut-js/api/I18nTranslation.html +1 -1
  367. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  368. data/docs/brut-js/api/LocaleDetection.html +1 -1
  369. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  370. data/docs/brut-js/api/Logger.html +1 -1
  371. data/docs/brut-js/api/Logger.js.html +1 -1
  372. data/docs/brut-js/api/Message.html +1 -1
  373. data/docs/brut-js/api/Message.js.html +1 -1
  374. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  375. data/docs/brut-js/api/RichString.html +1 -1
  376. data/docs/brut-js/api/RichString.js.html +1 -1
  377. data/docs/brut-js/api/Tabs.html +1 -1
  378. data/docs/brut-js/api/Tabs.js.html +1 -1
  379. data/docs/brut-js/api/Tracing.html +1 -1
  380. data/docs/brut-js/api/Tracing.js.html +1 -1
  381. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  382. data/docs/brut-js/api/external-Performance.html +1 -1
  383. data/docs/brut-js/api/external-Promise.html +1 -1
  384. data/docs/brut-js/api/external-ValidityState.html +1 -1
  385. data/docs/brut-js/api/external-Window.html +1 -1
  386. data/docs/brut-js/api/external-fetch.html +1 -1
  387. data/docs/brut-js/api/global.html +1 -1
  388. data/docs/brut-js/api/index.html +1 -1
  389. data/docs/brut-js/api/index.js.html +1 -1
  390. data/docs/brut-js/api/module-testing.html +1 -1
  391. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  392. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  393. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  394. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  395. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  396. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  397. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  398. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  399. data/docs/brut-js/api/testing_index.js.html +1 -1
  400. data/docs/brut-js.html +7 -7
  401. data/docs/business-logic.html +7 -7
  402. data/docs/cli.html +7 -7
  403. data/docs/components.html +10 -10
  404. data/docs/configuration.html +7 -7
  405. data/docs/css.html +7 -7
  406. data/docs/custom-element-tests.html +7 -7
  407. data/docs/database-access.html +7 -7
  408. data/docs/database-schema.html +7 -7
  409. data/docs/deployment.html +7 -7
  410. data/docs/dev-environment.html +7 -7
  411. data/docs/dir-structure.html +7 -7
  412. data/docs/doc-conventions.html +7 -7
  413. data/docs/end-to-end-tests.html +7 -7
  414. data/docs/features.html +7 -7
  415. data/docs/flash-and-session.html +7 -7
  416. data/docs/form-constraints.html +7 -7
  417. data/docs/forms.html +8 -8
  418. data/docs/getting-started.html +9 -9
  419. data/docs/handlers.html +7 -7
  420. data/docs/hashmap.json +1 -1
  421. data/docs/hooks.html +7 -7
  422. data/docs/i18n.html +7 -7
  423. data/docs/index.html +6 -6
  424. data/docs/instrumentation.html +7 -7
  425. data/docs/javascript.html +7 -7
  426. data/docs/jobs.html +7 -7
  427. data/docs/keyword-injection.html +7 -7
  428. data/docs/layouts.html +42 -12
  429. data/docs/lsp.html +7 -7
  430. data/docs/markdown-examples.html +7 -7
  431. data/docs/middleware.html +7 -7
  432. data/docs/overview.html +7 -7
  433. data/docs/pages.html +7 -7
  434. data/docs/recipes/alternate-layouts.html +8 -8
  435. data/docs/recipes/authentication.html +7 -7
  436. data/docs/recipes/custom-flash.html +8 -8
  437. data/docs/recipes/form-errors.html +7 -7
  438. data/docs/recipes/indexed-forms.html +7 -7
  439. data/docs/recipes/migrations.html +7 -7
  440. data/docs/recipes/text-field-component.html +7 -7
  441. data/docs/roadmap.html +7 -7
  442. data/docs/routes.html +7 -7
  443. data/docs/security.html +7 -7
  444. data/docs/seed-data.html +7 -7
  445. data/docs/space-time-continuum.html +7 -7
  446. data/docs/tutorial.html +7 -7
  447. data/docs/tutorials/01-intro.html +7 -7
  448. data/docs/tutorials/02-dialog.html +7 -7
  449. data/docs/unit-tests.html +7 -7
  450. data/docs/why.html +7 -7
  451. data/lib/brut/version.rb +1 -1
  452. data/mkbrut/Gemfile.lock +1 -1
  453. data/mkbrut/lib/mkbrut/version.rb +1 -1
  454. metadata +114 -115
  455. data/docs/assets/adrs.md.BxjHi9-8.js +0 -1
  456. data/docs/assets/adrs.md.BxjHi9-8.lean.js +0 -1
  457. data/docs/assets/ai.md.Cy9GWnER.js +0 -1
  458. data/docs/assets/ai.md.Cy9GWnER.lean.js +0 -1
  459. data/docs/assets/assets.md.7C3HWkga.lean.js +0 -1
  460. data/docs/assets/brut-js.md.B4GYxQVw.lean.js +0 -1
  461. data/docs/assets/business-logic.md.BY4hGy0m.js +0 -1
  462. data/docs/assets/business-logic.md.BY4hGy0m.lean.js +0 -1
  463. data/docs/assets/chunks/@localSearchIndexroot.BWVzhs5N.js +0 -1
  464. data/docs/assets/chunks/VPLocalSearchBox.DCJk5nAW.js +0 -8
  465. data/docs/assets/chunks/framework.1L-BeKqY.js +0 -18
  466. data/docs/assets/cli.md.CjsktgFz.lean.js +0 -1
  467. data/docs/assets/components.md.rMhQ0WdZ.lean.js +0 -1
  468. data/docs/assets/configuration.md.BK42Yjp_.lean.js +0 -1
  469. data/docs/assets/css.md.CltvJqAa.lean.js +0 -1
  470. data/docs/assets/custom-element-tests.md.B_rbta32.lean.js +0 -1
  471. data/docs/assets/database-access.md.gnluu54N.lean.js +0 -1
  472. data/docs/assets/database-schema.md.LpmBPVEU.lean.js +0 -1
  473. data/docs/assets/deployment.md.BLseERGV.lean.js +0 -1
  474. data/docs/assets/dir-structure.md.CWir1pic.lean.js +0 -1
  475. data/docs/assets/doc-conventions.md.DOkAuXlt.js +0 -1
  476. data/docs/assets/doc-conventions.md.DOkAuXlt.lean.js +0 -1
  477. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +0 -1
  478. data/docs/assets/features.md.DPFXsy0z.lean.js +0 -1
  479. data/docs/assets/flash-and-session.md.nPvUpnUx.lean.js +0 -1
  480. data/docs/assets/form-constraints.md.KTv5cdR4.lean.js +0 -1
  481. data/docs/assets/forms.md.v9qIbmUM.lean.js +0 -1
  482. data/docs/assets/getting-started.md.DTOl4c2g.lean.js +0 -1
  483. data/docs/assets/handlers.md.h84MMB1R.lean.js +0 -1
  484. data/docs/assets/hooks.md.Jmb5VOLA.lean.js +0 -1
  485. data/docs/assets/i18n.md.BAm9t9JJ.lean.js +0 -1
  486. data/docs/assets/instrumentation.md._lNSriEZ.lean.js +0 -1
  487. data/docs/assets/javascript.md.DzrMxUmI.lean.js +0 -1
  488. data/docs/assets/jobs.md.S-2amAYp.js +0 -1
  489. data/docs/assets/jobs.md.S-2amAYp.lean.js +0 -1
  490. data/docs/assets/keyword-injection.md.95Zgh2eN.lean.js +0 -1
  491. data/docs/assets/layouts.md.CVGl9xIO.js +0 -38
  492. data/docs/assets/layouts.md.CVGl9xIO.lean.js +0 -1
  493. data/docs/assets/lsp.md.Dn1rIiW0.js +0 -1
  494. data/docs/assets/lsp.md.Dn1rIiW0.lean.js +0 -1
  495. data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +0 -1
  496. data/docs/assets/middleware.md.Czz_UlJN.lean.js +0 -1
  497. data/docs/assets/overview.md.DlKiRRG_.js +0 -1
  498. data/docs/assets/overview.md.DlKiRRG_.lean.js +0 -1
  499. data/docs/assets/pages.md.B7Hc-i6H.lean.js +0 -1
  500. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +0 -1
  501. data/docs/assets/recipes_authentication.md.nwO6F7Ou.lean.js +0 -1
  502. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +0 -15
  503. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +0 -1
  504. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +0 -1
  505. data/docs/assets/recipes_form-errors.md.Bv5RCKqH.lean.js +0 -1
  506. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +0 -1
  507. data/docs/assets/recipes_migrations.md.CTcnWDJF.lean.js +0 -1
  508. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +0 -1
  509. data/docs/assets/roadmap.md.C6PRi0DX.js +0 -1
  510. data/docs/assets/roadmap.md.C6PRi0DX.lean.js +0 -1
  511. data/docs/assets/routes.md.BD6y2i-f.lean.js +0 -1
  512. data/docs/assets/security.md.C0G_AZR-.js +0 -1
  513. data/docs/assets/security.md.C0G_AZR-.lean.js +0 -1
  514. data/docs/assets/seed-data.md.BvFZlqIk.lean.js +0 -1
  515. data/docs/assets/space-time-continuum.md.xl44xDos.js +0 -1
  516. data/docs/assets/space-time-continuum.md.xl44xDos.lean.js +0 -1
  517. data/docs/assets/tutorial.md.BM40jnoq.lean.js +0 -1
  518. data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +0 -1
  519. data/docs/assets/why.md.C-hk5xgJ.js +0 -1
  520. data/docs/assets/why.md.C-hk5xgJ.lean.js +0 -1
  521. data/docs/recipes/blank-layouts.html +0 -43
@@ -1,4 +1,4 @@
1
- import{_ as t,c as s,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Keyword Injection","description":"","frontmatter":{},"headers":[],"relativePath":"keyword-injection.md","filePath":"keyword-injection.md"}'),n={name:"keyword-injection.md"};function o(r,e,l,d,h,p){return a(),s("div",null,e[0]||(e[0]=[i(`<h1 id="keyword-injection" tabindex="-1">Keyword Injection <a class="header-anchor" href="#keyword-injection" aria-label="Permalink to &quot;Keyword Injection&quot;">​</a></h1><p>Brut is desiged around classes and objects, as compared to modules and DSLs. Almost everything you do when building your app is to create a class that has an initializer and implements one or more methods. But, these classes often need information from the request that Brut is managing.</p><p>In a basic Rack or Sinatra app, you would access this information via Rack&#39;s API, which is essentially a Hash of Whatever. It&#39;s error-prone and requires consulting documentation, source code, or runtime information to figure out what&#39;s stored where.</p><p>Brut can instead inject these values explicitly into the classes of yours it creates. It does this based on the names of keyword arguments declared by your class&#39; intializer.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>A <a href="/pages.html">Page</a> may need the session, flash, HTTP headers, query string parameters, or placeholder values from the URI. These can all be provided by declaring them as keyword arguments to the page&#39;s initializer:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">clas </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsByIdPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
1
+ import{_ as t,c as s,o as a,ag as i}from"./chunks/framework.C4nOkCZI.js";const u=JSON.parse('{"title":"Keyword Injection","description":"","frontmatter":{},"headers":[],"relativePath":"keyword-injection.md","filePath":"keyword-injection.md"}'),n={name:"keyword-injection.md"};function o(r,e,l,d,h,p){return a(),s("div",null,[...e[0]||(e[0]=[i(`<h1 id="keyword-injection" tabindex="-1">Keyword Injection <a class="header-anchor" href="#keyword-injection" aria-label="Permalink to &quot;Keyword Injection&quot;">​</a></h1><p>Brut is desiged around classes and objects, as compared to modules and DSLs. Almost everything you do when building your app is to create a class that has an initializer and implements one or more methods. But, these classes often need information from the request that Brut is managing.</p><p>In a basic Rack or Sinatra app, you would access this information via Rack&#39;s API, which is essentially a Hash of Whatever. It&#39;s error-prone and requires consulting documentation, source code, or runtime information to figure out what&#39;s stored where.</p><p>Brut can instead inject these values explicitly into the classes of yours it creates. It does this based on the names of keyword arguments declared by your class&#39; intializer.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>A <a href="/pages.html">Page</a> may need the session, flash, HTTP headers, query string parameters, or placeholder values from the URI. These can all be provided by declaring them as keyword arguments to the page&#39;s initializer:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">clas </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsByIdPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
2
2
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># &quot;:id&quot; from /widgets/:id</span></span>
3
3
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># AppSession instance for this request</span></span>
4
4
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> flash:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Flash for this request</span></span>
@@ -18,4 +18,4 @@ import{_ as t,c as s,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u
18
18
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">authenticated_account:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
19
19
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
20
20
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
21
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If the request context has no value for <code>authenticated_account</code>, the page cannot be instantiated. Thus, the page&#39;s code can always rely on a non-<code>nil</code> value for <code>authenticated_account</code> (provided you don&#39;t inject <code>nil</code>).</p><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p>Do not inject <code>nil</code> into the request context. Brut currently allows it, but may prevent it in a future update. <code>nil</code> is no good for nobody.</p></div><h3 id="when-values-aren-t-available" tabindex="-1">When Values Aren&#39;t Available <a class="header-anchor" href="#when-values-aren-t-available" aria-label="Permalink to &quot;When Values Aren&#39;t Available&quot;">​</a></h3><p>When a value is not available for injection, and the keyword doesn&#39;t provide a default, Brut will raise an error. This is because such a situation represents a design error.</p><p>The tables above document which values should always be available. You should never provide a default value for these, e.g. <code>session:</code> or <code>env:</code>. For values that are not always available, you should provide a default value unless you are sure there will be no routing to the page or handler without the value set.</p><p>This is most important for query string parameters. Since a user can easily manipulate these, if your page accepts, say, the parameter <code>use_detailed_view</code>, but that parameter isn&#39;t present, Brut will not be able to instantiate your page unless <code>use_detailed_view:</code> has a default value in the initializer&#39;s keyword arguments.</p><p>See <a href="/hooks.html">route hooks</a>.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Brut will not create your classes in a test. Instead, you must pass in the values you want. There are various helpers in <a href="/api/Brut/SpecSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport</code></a> to create blank or empty versions of the special classes.</p><p>In particular, A basic <code>request_context</code> is setup per test and injected into the Thread local storage. This means that if your test should trigger a codepath that <em>does</em> cause Brut to use keyword injection, useful values will be injected.</p><p>For your tests, however, you should pass in directly what you need:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;">page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsByIdPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> empty_session)</span></span></code></pre></div><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Consider a method like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create_widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">organization:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">quantity:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>Outside of Brut, the way to interpret this arguments is as follows:</p><ul><li><code>name</code> is required</li><li><code>organization</code> is optional</li><li><code>quantity</code> has a default value of 10 if not provided</li></ul><p>Any method or intializer that will be keyword-injected should be designed with this in mind. Thus, the following guidelines will be helpful in managing your app:</p><ul><li><strong>Do not provide default values when Brut documents the value is always available</strong><ul><li>If your page needs the session, it will always be there. Don&#39;t default <code>session:</code> to some other value (especially <code>nil</code>!)</li></ul></li><li><strong>Choose arguments based on the needs of the class:</strong><ul><li>If a value is optional, default it to either <code>nil</code> or a symbol that indicates what happens when the value is omitted</li><li>If an optional value has a default, use that (this should be rare for pages, handlers, components, and hooks)</li><li>Otherwise, do not provide a default for the keyword</li></ul></li><li><strong>Design for non-<code>nil</code> values instead of allowing <code>nil</code> and checking for it</strong><ul><li>If a page needs, say, the currently logged-in user, set that up as injectible with no default.</li><li>If a codepath creates that page without the logged-in user, you will get a very obvious error and can figure out how it happened. Your page&#39;s code doesn&#39;t need to figure out what to do with <code>nil</code></li></ul></li><li><strong>Do not inject <code>nil</code> into the request context.</strong> When your code requires a value for a keyword, you want to rely on that value being non-nil. Thus, avoid injecting <code>nil</code> into the request context. Brut will allow it as a sort-of escape hatch, but you should design your app to avoid it</li><li><strong>Be careful injecting global data.</strong> The request context instance is per request, but you could certainly put global data into it. For example, you may put an initialized API client into the request context as a convieniece. <strong>Be careful</strong> because your app is multi-threaded. Any object that is not scoped to the request must be thread-safe.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 7, 2025</em></p><p>Keyword injection is currently implemented in a few places and not available via public API. It could be useful as an API and it will be exposed at some point. For now, it&#39;s only available for Brut-managed classes as documented here.</p>`,50)]))}const k=t(n,[["render",o]]);export{u as __pageData,k as default};
21
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If the request context has no value for <code>authenticated_account</code>, the page cannot be instantiated. Thus, the page&#39;s code can always rely on a non-<code>nil</code> value for <code>authenticated_account</code> (provided you don&#39;t inject <code>nil</code>).</p><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p>Do not inject <code>nil</code> into the request context. Brut currently allows it, but may prevent it in a future update. <code>nil</code> is no good for nobody.</p></div><h3 id="when-values-aren-t-available" tabindex="-1">When Values Aren&#39;t Available <a class="header-anchor" href="#when-values-aren-t-available" aria-label="Permalink to &quot;When Values Aren&#39;t Available&quot;">​</a></h3><p>When a value is not available for injection, and the keyword doesn&#39;t provide a default, Brut will raise an error. This is because such a situation represents a design error.</p><p>The tables above document which values should always be available. You should never provide a default value for these, e.g. <code>session:</code> or <code>env:</code>. For values that are not always available, you should provide a default value unless you are sure there will be no routing to the page or handler without the value set.</p><p>This is most important for query string parameters. Since a user can easily manipulate these, if your page accepts, say, the parameter <code>use_detailed_view</code>, but that parameter isn&#39;t present, Brut will not be able to instantiate your page unless <code>use_detailed_view:</code> has a default value in the initializer&#39;s keyword arguments.</p><p>See <a href="/hooks.html">route hooks</a>.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Brut will not create your classes in a test. Instead, you must pass in the values you want. There are various helpers in <a href="/api/Brut/SpecSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport</code></a> to create blank or empty versions of the special classes.</p><p>In particular, A basic <code>request_context</code> is setup per test and injected into the Thread local storage. This means that if your test should trigger a codepath that <em>does</em> cause Brut to use keyword injection, useful values will be injected.</p><p>For your tests, however, you should pass in directly what you need:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;">page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsByIdPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> empty_session)</span></span></code></pre></div><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Consider a method like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create_widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">organization:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">quantity:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>Outside of Brut, the way to interpret this arguments is as follows:</p><ul><li><code>name</code> is required</li><li><code>organization</code> is optional</li><li><code>quantity</code> has a default value of 10 if not provided</li></ul><p>Any method or intializer that will be keyword-injected should be designed with this in mind. Thus, the following guidelines will be helpful in managing your app:</p><ul><li><strong>Do not provide default values when Brut documents the value is always available</strong><ul><li>If your page needs the session, it will always be there. Don&#39;t default <code>session:</code> to some other value (especially <code>nil</code>!)</li></ul></li><li><strong>Choose arguments based on the needs of the class:</strong><ul><li>If a value is optional, default it to either <code>nil</code> or a symbol that indicates what happens when the value is omitted</li><li>If an optional value has a default, use that (this should be rare for pages, handlers, components, and hooks)</li><li>Otherwise, do not provide a default for the keyword</li></ul></li><li><strong>Design for non-<code>nil</code> values instead of allowing <code>nil</code> and checking for it</strong><ul><li>If a page needs, say, the currently logged-in user, set that up as injectible with no default.</li><li>If a codepath creates that page without the logged-in user, you will get a very obvious error and can figure out how it happened. Your page&#39;s code doesn&#39;t need to figure out what to do with <code>nil</code></li></ul></li><li><strong>Do not inject <code>nil</code> into the request context.</strong> When your code requires a value for a keyword, you want to rely on that value being non-nil. Thus, avoid injecting <code>nil</code> into the request context. Brut will allow it as a sort-of escape hatch, but you should design your app to avoid it</li><li><strong>Be careful injecting global data.</strong> The request context instance is per request, but you could certainly put global data into it. For example, you may put an initialized API client into the request context as a convieniece. <strong>Be careful</strong> because your app is multi-threaded. Any object that is not scoped to the request must be thread-safe.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 7, 2025</em></p><p>Keyword injection is currently implemented in a few places and not available via public API. It could be useful as an API and it will be exposed at some point. For now, it&#39;s only available for Brut-managed classes as documented here.</p>`,50)])])}const k=t(n,[["render",o]]);export{u as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as t,c as s,o as a,ag as i}from"./chunks/framework.C4nOkCZI.js";const u=JSON.parse('{"title":"Keyword Injection","description":"","frontmatter":{},"headers":[],"relativePath":"keyword-injection.md","filePath":"keyword-injection.md"}'),n={name:"keyword-injection.md"};function o(r,e,l,d,h,p){return a(),s("div",null,[...e[0]||(e[0]=[i("",50)])])}const k=t(n,[["render",o]]);export{u as __pageData,k as default};
@@ -0,0 +1,68 @@
1
+ import{_ as i,c as a,o as t,ag as n}from"./chunks/framework.C4nOkCZI.js";const y=JSON.parse('{"title":"Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"layouts.md","filePath":"layouts.md"}'),e={name:"layouts.md"};function l(h,s,p,k,r,d){return t(),a("div",null,[...s[0]||(s[0]=[n(`<h1 id="layouts" tabindex="-1">Layouts <a class="header-anchor" href="#layouts" aria-label="Permalink to &quot;Layouts&quot;">​</a></h1><p>Brut supports <em>layouts</em>, which are a way to centralizing common HTML amongst many different pages. Conceptually, they are the same as a Rails layout. Technically, they are a Phlex component designed to render a page from a <code>yield</code> block.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Your app should include <code>app/src/front_end/layouts/default_layout.rb</code>. The name &quot;default&quot; isn&#39;t special, it&#39;s just what the <code>layout</code> method from <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a> returns.</p><p>A layout is a Phlex component that&#39;s expected to have a single call to <code>yield</code> in its <code>view_template</code> method.</p><h3 id="default-layout-and-common-layout-needs" tabindex="-1">Default Layout and Common Layout Needs <a class="header-anchor" href="#default-layout-and-common-layout-needs" aria-label="Permalink to &quot;Default Layout and Common Layout Needs&quot;">​</a></h3><p>Here is the <code>DefaultLayout</code> provided to new Brut apps:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DefaultLayout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Layout</span></span>
2
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
3
+ <span class="line"></span>
4
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">page:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
5
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @page </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page</span></span>
6
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
7
+ <span class="line"></span>
8
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
9
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> doctype</span></span>
10
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">lang:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;en&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
11
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> head </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
12
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">charset:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;utf-8&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
13
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">content:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;width=device-width,initial-scale=1&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;viewport&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
14
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">content:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;website&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">property:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;og:type&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
15
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> link</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">rel:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;manifest&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;/static/manifest.json&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
16
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> link</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">rel:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;preload&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">as:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;style&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> asset_path</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/css/styles.css&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">))</span></span>
17
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> link</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">rel:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;stylesheet&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> asset_path</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/css/styles.css&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">))</span></span>
18
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">defer:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">src:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> asset_path</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/js/app.js&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">))</span></span>
19
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title { app_name }</span></span>
20
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> PageIdentifier</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(page)</span></span>
21
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> I18nTranslations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.cs&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
22
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> I18nTranslations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.this_field&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
23
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Traceparent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">()</span></span>
24
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
25
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RequestContext</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">inject</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
26
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">LocaleDetection</span></span>
27
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
28
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
29
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
30
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> body </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
31
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> brut_tracing </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">url:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;/__brut/instrumentation&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">show_warnings:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
32
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> main </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">page_name</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
33
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> yield</span></span>
34
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
35
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
36
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
37
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
38
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You will likely want to customize what&#39;s in your layout, but a few components included by default are important for other features of Brut:</p><table tabindex="0"><thead><tr><th>Component</th><th>Purpose</th></tr></thead><tbody><tr><td><a href="/api/Brut/FrontEnd/Components/PageIdentifier.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::PageIdentifier</code></a></td><td>Creates a <code>&lt;meta&gt;</code> tag with the page&#39;s name in it, which is handy for managing your end-to-end tests.</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/I18nTranslations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::I18nTranslations</code></a></td><td>Includes translatsion for common client-side constraint violations. These are used by <a href="/brut-js/api/ConstraintViolationMessages.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv-messages&gt;</code></a> and <code>,brut-cv&gt;</code>. See <a href="/forms.html">Forms</a>, <a href="/i18n.html">I18n</a>, and <a href="/javascript.html">JavaScript</a> for more details</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/Traceparent.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Traceparent</code></a></td><td>Includes the OpenTelemetry <em>traceparent</em> on the page so that client-side telemetry is reported back to the server. See <a href="/brut-js/api/Tracing.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-tracing&gt;</code></a> and <a href="/instrumentation.html">observability</a></td></tr><tr><td><a href="/brut-js/api/Tracing.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-tracing&gt;</code></a> / <code>brut_tracing</code></td><td>Custom element that collects the client-side telemetry and sends it back to the server. See <a href="/instrumentation.html">observability</a></td></tr></tbody></table><h3 id="adding-logic-dynamic-behavior-to-layouts" tabindex="-1">Adding Logic/Dynamic Behavior to Layouts <a class="header-anchor" href="#adding-logic-dynamic-behavior-to-layouts" aria-label="Permalink to &quot;Adding Logic/Dynamic Behavior to Layouts&quot;">​</a></h3><p>Often, your pages will need to make slight tweaks to the layout that don&#39;t apply to all pages. For example, you may wish for a certain page to refresh on a schedule and you want to do that with a <a href="https://en.wikipedia.org/wiki/Meta_refresh" target="_blank" rel="noreferrer">meta refresh</a>, which must appear in the <code>&lt;head&gt;</code> of the page.</p><p>Unlike Rails, which uses named blocks to render optional or dynamic content, Brut allows you to use methods and normal Ruby-based flow logic. Since your layouts have access to the page they are laying out, you can use your pages&#39; APIs to do whatever it is you need.</p><p>Taking the meta refresh example, suppose your <code>AppPage</code> defines a method, <code>auto_refresh_seconds</code> that, if non-<code>nil</code> means your page should automatically reload itself after that many seconds. By default, you don&#39;t refresh, so it returns <code>nil</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">page</span></span>
39
+ <span class="line"></span>
40
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
41
+ <span class="line"></span>
42
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> auto_refresh_seconds</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> nil</span></span>
43
+ <span class="line"></span>
44
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
45
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Your layout can refrence this API, since it&#39;s just a method on a class:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DefaultLayout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Layout</span></span>
46
+ <span class="line"></span>
47
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
48
+ <span class="line"></span>
49
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
50
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> doctype</span></span>
51
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">lang:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;en&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
52
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> head </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
53
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">auto_refresh_seconds</span></span>
54
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> meta</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">http_equiv:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> safe</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;refresh&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">), </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">content:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">auto_refresh_seconds</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
55
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
56
+ <span class="line"></span>
57
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
58
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
59
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
60
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Since your pages are a class hierarchy, you can override <code>auto_refresh_seconds</code> in any page, and that page will automatically refresh itself:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DashboardPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
61
+ <span class="line"></span>
62
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> auto_refresh_seconds</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 60</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> *</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 60</span></span>
63
+ <span class="line"></span>
64
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
65
+ <span class="line"></span>
66
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h3 id="alternate-layouts" tabindex="-1">Alternate Layouts <a class="header-anchor" href="#alternate-layouts" aria-label="Permalink to &quot;Alternate Layouts&quot;">​</a></h3><p>If you used <code>mkbrut</code>, you should have access to a <code>BlankLayout</code> that is useful for allowing a page to respond to Ajax requests:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SomePage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
67
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> layout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;blank&quot;</span></span>
68
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>See <a href="/recipes/alternate-layouts.html">creating alternate layouts</a> for more information on creating alternate layouts based on your needs.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>You generally don&#39;t test a layout, aside from end-to-end tests. If your layout needs complex logic, you are encouraged to extract that to a <a href="/components.html">component</a> and test that instead.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Layouts can use components, just keep in mind that any data a component needs must be passed to its initializer. Since the layout doesn&#39;t have access to the page, this implies that components used in your layout must either not require dynamic data or be <a href="/components.html#global-components">global components</a></p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated Sep 9, 2025</em></p><p>Layouts work due to the implementation of the method <code>view_template</code> in <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>. This is why a page class must provide <code>page_template</code> instead.</p>`,31)])])}const c=i(e,[["render",l]]);export{y as __pageData,c as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as a,o as t,ag as n}from"./chunks/framework.C4nOkCZI.js";const y=JSON.parse('{"title":"Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"layouts.md","filePath":"layouts.md"}'),e={name:"layouts.md"};function l(h,s,p,k,r,d){return t(),a("div",null,[...s[0]||(s[0]=[n("",31)])])}const c=i(e,[["render",l]]);export{y as __pageData,c as default};
@@ -0,0 +1 @@
1
+ import{_ as t,c as o,o as r,ag as a}from"./chunks/framework.C4nOkCZI.js";const h=JSON.parse('{"title":"Language Server Protocol (LSP) Support","description":"","frontmatter":{},"headers":[],"relativePath":"lsp.md","filePath":"lsp.md"}'),s={name:"lsp.md"};function d(i,e,n,l,c,u){return r(),o("div",null,[...e[0]||(e[0]=[a('<h1 id="language-server-protocol-lsp-support" tabindex="-1">Language Server Protocol (LSP) Support <a class="header-anchor" href="#language-server-protocol-lsp-support" aria-label="Permalink to &quot;Language Server Protocol (LSP) Support&quot;">​</a></h1><p>Because Brut development happens inside Docker, but your editor likely runs on your computer, getting LSP servers running takes a few more steps.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>When you created your app with <code>mkbrut</code>, the following LSP-related modules are set up and/or installed:</p><ul><li>Shopify&#39;s Ruby LSP server (installed from <code>bin/setup</code>)</li><li>Microsoft&#39;s TypeScript/JavaScript and CSS LSP serfvers (specified in <code>package.json</code>, installed when <code>npm install</code> runs from <code>bin/setup</code>)</li></ul><p>In order to use them from your computer a few configurations are needed, some of which Brut has done, and some you will need to do.</p><table tabindex="0"><thead><tr><th>Configuration</th><th>Description</th><th>Brut Handled?</th></tr></thead><tbody><tr><td>Paths inside Docker Must Match Your Computer</td><td>When an LSP server communicates about a file, it does so with a path. That means that paths inside the Docker container must be the same as those on your computer. Brut achievecs this by using <code>${CWD}</code> inside <code>docker-compose.dx.yml</code></td><td>✅</td></tr><tr><td>Third party libraries must <em>also</em> be installed in a path that is the same in both places</td><td>When jumping to a definition, the LSP server will again use paths, which must match. Because Node modules are installed local to your app, they already work. Ruby Gems, however, are configured to be installed in <code>local-gems</code> in your app. Brut should&#39;ve added this to <code>.gitignore</code> and setup everything inside Docker to use it.</td><td>✅</td></tr><tr><td>Your editor must use <code>dx/exec</code> to execute LSP commands</td><td>Your editor will need to know that the LSP servers are running inside Docker. If your editor allows configuring the commands used to do this, you must prefix them with <code>dx/exec</code>. See <a href="https://naildrivin5.com/blog/2025/06/12/neovim-and-lsp-servers-working-with-docker-based-development.html" target="_blank" rel="noreferrer">my blog post</a> for details.</td><td>❌</td></tr><tr><td>Other languages or plugins to existing LSP servers</td><td>I haven&#39;t used these, so no idea how well they work.</td><td>❌</td></tr></tbody></table>',7)])])}const m=t(s,[["render",d]]);export{h as __pageData,m as default};
@@ -0,0 +1 @@
1
+ import{_ as t,c as o,o as r,ag as a}from"./chunks/framework.C4nOkCZI.js";const h=JSON.parse('{"title":"Language Server Protocol (LSP) Support","description":"","frontmatter":{},"headers":[],"relativePath":"lsp.md","filePath":"lsp.md"}'),s={name:"lsp.md"};function d(i,e,n,l,c,u){return r(),o("div",null,[...e[0]||(e[0]=[a("",7)])])}const m=t(s,[["render",d]]);export{h as __pageData,m as default};
@@ -1,4 +1,4 @@
1
- import{_ as a,c as i,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Markdown Extension Examples","description":"","frontmatter":{},"headers":[],"relativePath":"markdown-examples.md","filePath":"markdown-examples.md"}'),e={name:"markdown-examples.md"};function l(p,s,h,k,r,d){return n(),i("div",null,s[0]||(s[0]=[t(`<h1 id="markdown-extension-examples" tabindex="-1">Markdown Extension Examples <a class="header-anchor" href="#markdown-extension-examples" aria-label="Permalink to &quot;Markdown Extension Examples&quot;">​</a></h1><p>This page demonstrates some of the built-in markdown extensions provided by VitePress.</p><h2 id="syntax-highlighting" tabindex="-1">Syntax Highlighting <a class="header-anchor" href="#syntax-highlighting" aria-label="Permalink to &quot;Syntax Highlighting&quot;">​</a></h2><p>VitePress provides Syntax Highlighting powered by <a href="https://github.com/shikijs/shiki" target="_blank" rel="noreferrer">Shiki</a>, with additional features like line-highlighting:</p><p><strong>Input</strong></p><div class="language-md vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">md</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">\`\`\`js{4}</span></span>
1
+ import{_ as a,c as i,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const E=JSON.parse('{"title":"Markdown Extension Examples","description":"","frontmatter":{},"headers":[],"relativePath":"markdown-examples.md","filePath":"markdown-examples.md"}'),e={name:"markdown-examples.md"};function l(p,s,h,k,r,d){return n(),i("div",null,[...s[0]||(s[0]=[t(`<h1 id="markdown-extension-examples" tabindex="-1">Markdown Extension Examples <a class="header-anchor" href="#markdown-extension-examples" aria-label="Permalink to &quot;Markdown Extension Examples&quot;">​</a></h1><p>This page demonstrates some of the built-in markdown extensions provided by VitePress.</p><h2 id="syntax-highlighting" tabindex="-1">Syntax Highlighting <a class="header-anchor" href="#syntax-highlighting" aria-label="Permalink to &quot;Syntax Highlighting&quot;">​</a></h2><p>VitePress provides Syntax Highlighting powered by <a href="https://github.com/shikijs/shiki" target="_blank" rel="noreferrer">Shiki</a>, with additional features like line-highlighting:</p><p><strong>Input</strong></p><div class="language-md vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">md</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">\`\`\`js{4}</span></span>
2
2
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">export</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> default</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
3
3
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> data</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> () {</span></span>
4
4
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
@@ -30,4 +30,4 @@ import{_ as a,c as i,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
30
30
  <span class="line"></span>
31
31
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::: details</span></span>
32
32
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">This is a details block.</span></span>
33
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:::</span></span></code></pre></div><p><strong>Output</strong></p><div class="info custom-block"><p class="custom-block-title">INFO</p><p>This is an info box.</p></div><div class="tip custom-block"><p class="custom-block-title">TIP</p><p>This is a tip.</p></div><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>This is a warning.</p></div><div class="danger custom-block"><p class="custom-block-title">DANGER</p><p>This is a dangerous warning.</p></div><details class="details custom-block"><summary>Details</summary><p>This is a details block.</p></details><h2 id="more" tabindex="-1">More <a class="header-anchor" href="#more" aria-label="Permalink to &quot;More&quot;">​</a></h2><p>Check out the documentation for the <a href="https://vitepress.dev/guide/markdown" target="_blank" rel="noreferrer">full list of markdown extensions</a>.</p>`,19)]))}const c=a(e,[["render",l]]);export{E as __pageData,c as default};
33
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">:::</span></span></code></pre></div><p><strong>Output</strong></p><div class="info custom-block"><p class="custom-block-title">INFO</p><p>This is an info box.</p></div><div class="tip custom-block"><p class="custom-block-title">TIP</p><p>This is a tip.</p></div><div class="warning custom-block"><p class="custom-block-title">WARNING</p><p>This is a warning.</p></div><div class="danger custom-block"><p class="custom-block-title">DANGER</p><p>This is a dangerous warning.</p></div><details class="details custom-block"><summary>Details</summary><p>This is a details block.</p></details><h2 id="more" tabindex="-1">More <a class="header-anchor" href="#more" aria-label="Permalink to &quot;More&quot;">​</a></h2><p>Check out the documentation for the <a href="https://vitepress.dev/guide/markdown" target="_blank" rel="noreferrer">full list of markdown extensions</a>.</p>`,19)])])}const c=a(e,[["render",l]]);export{E as __pageData,c as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as i,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const E=JSON.parse('{"title":"Markdown Extension Examples","description":"","frontmatter":{},"headers":[],"relativePath":"markdown-examples.md","filePath":"markdown-examples.md"}'),e={name:"markdown-examples.md"};function l(p,s,h,k,r,d){return n(),i("div",null,[...s[0]||(s[0]=[t("",19)])])}const c=a(e,[["render",l]]);export{E as __pageData,c as default};
@@ -1,4 +1,4 @@
1
- import{_ as a,c as i,o as e,ag as t}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Middleware","description":"","frontmatter":{},"headers":[],"relativePath":"middleware.md","filePath":"middleware.md"}'),n={name:"middleware.md"};function l(h,s,p,d,r,o){return e(),i("div",null,s[0]||(s[0]=[t(`<h1 id="middleware" tabindex="-1">Middleware <a class="header-anchor" href="#middleware" aria-label="Permalink to &quot;Middleware&quot;">​</a></h1><p>Brut supports Rack Middleware.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Similar to <a href="/hooks.html">route hooks</a>, Brut supports Rack Middleware, which is a lower-level way of modifying a request or changing behavior.</p><p>Middleware is recommended if what you want to do is not dependent on your application&#39;s code or classes and is relatively simple.</p><p>To use a middleware, create the class in <code>app/src/front_end/middleware/</code>. You are encouraged to extend <a href="/api/Brut/FrontEnd/Middleware.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Middleware</code></a>, however this is an empty class currently. It could grow to have helper methods you&#39;ll find useful.</p><p>The class itself should conform to Rack&#39;s specification, which is typically that it will be given the Rack &quot;app&quot; in the initializer, and then have a method <code>call</code> which will be given the Rack environment.</p><p>Here&#39;s a middleware that adds a tag to the environment for paths that are &quot;special&quot; to our app, which in this case means they start with <code>/special</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> TagSpecialPathsMiddleware</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Middleware</span></span>
1
+ import{_ as a,c as i,o as e,ag as t}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Middleware","description":"","frontmatter":{},"headers":[],"relativePath":"middleware.md","filePath":"middleware.md"}'),n={name:"middleware.md"};function l(h,s,p,d,r,o){return e(),i("div",null,[...s[0]||(s[0]=[t(`<h1 id="middleware" tabindex="-1">Middleware <a class="header-anchor" href="#middleware" aria-label="Permalink to &quot;Middleware&quot;">​</a></h1><p>Brut supports Rack Middleware.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Similar to <a href="/hooks.html">route hooks</a>, Brut supports Rack Middleware, which is a lower-level way of modifying a request or changing behavior.</p><p>Middleware is recommended if what you want to do is not dependent on your application&#39;s code or classes and is relatively simple.</p><p>To use a middleware, create the class in <code>app/src/front_end/middleware/</code>. You are encouraged to extend <a href="/api/Brut/FrontEnd/Middleware.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Middleware</code></a>, however this is an empty class currently. It could grow to have helper methods you&#39;ll find useful.</p><p>The class itself should conform to Rack&#39;s specification, which is typically that it will be given the Rack &quot;app&quot; in the initializer, and then have a method <code>call</code> which will be given the Rack environment.</p><p>Here&#39;s a middleware that adds a tag to the environment for paths that are &quot;special&quot; to our app, which in this case means they start with <code>/special</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> TagSpecialPathsMiddleware</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Middleware</span></span>
2
2
  <span class="line"></span>
3
3
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initializer</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(app)</span></span>
4
4
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @app </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> app</span></span>
@@ -17,4 +17,4 @@ import{_ as a,c as i,o as e,ag as t}from"./chunks/framework.1L-BeKqY.js";const c
17
17
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> use </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:TagSpecialPathsMiddleware</span></span>
18
18
  <span class="line"></span>
19
19
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
20
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Don&#39;t use the actual class as this can create load order issues.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Like hooks, Rack middleware can be tested as a normal class. That said, you are encouraged to test the middleware as part of an end-to-end test if possible, since this will ensure it&#39;s configured properly in the context of your app.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Middleware should be used when its logic can be entirely based on the Rack environment passed-in. While your database models and other classes should be available, excessive use of your domain logic in a middleware can create a confusing situation.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Route hooks and Middlewares do not share implementations, however they are similar in concept. These concepts may be unified in the future.</p><p><code>use</code> and the way Middleware behaves follows Sinatra&#39;s implementation as Brut is currently based on Sinatra. This may not always be the case, however as things change, we will do our best to ensure the semantics remain the same. Nevertheless, it&#39;s advisable to have end to end tests assert the behavior of your configured middleware and not just a unit test of the class itself.</p>`,21)]))}const g=a(n,[["render",l]]);export{c as __pageData,g as default};
20
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Don&#39;t use the actual class as this can create load order issues.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Like hooks, Rack middleware can be tested as a normal class. That said, you are encouraged to test the middleware as part of an end-to-end test if possible, since this will ensure it&#39;s configured properly in the context of your app.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Middleware should be used when its logic can be entirely based on the Rack environment passed-in. While your database models and other classes should be available, excessive use of your domain logic in a middleware can create a confusing situation.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Route hooks and Middlewares do not share implementations, however they are similar in concept. These concepts may be unified in the future.</p><p><code>use</code> and the way Middleware behaves follows Sinatra&#39;s implementation as Brut is currently based on Sinatra. This may not always be the case, however as things change, we will do our best to ensure the semantics remain the same. Nevertheless, it&#39;s advisable to have end to end tests assert the behavior of your configured middleware and not just a unit test of the class itself.</p>`,21)])])}const g=a(n,[["render",l]]);export{c as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as i,o as e,ag as t}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Middleware","description":"","frontmatter":{},"headers":[],"relativePath":"middleware.md","filePath":"middleware.md"}'),n={name:"middleware.md"};function l(h,s,p,d,r,o){return e(),i("div",null,[...s[0]||(s[0]=[t("",21)])])}const g=a(n,[["render",l]]);export{c as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as r,o as s,ag as t,j as o}from"./chunks/framework.C4nOkCZI.js";const i="/assets/OverviewMetro.DUS-5fUZ.png",g=JSON.parse('{"title":"Conceptual Overview","description":"","frontmatter":{},"headers":[],"relativePath":"overview.md","filePath":"overview.md"}'),n={name:"overview.md"};function l(d,e,c,h,u,p){return s(),r("div",null,[...e[0]||(e[0]=[t('<h1 id="conceptual-overview" tabindex="-1">Conceptual Overview <a class="header-anchor" href="#conceptual-overview" aria-label="Permalink to &quot;Conceptual Overview&quot;">​</a></h1><p>Brut is a way to build web apps that generate HTML, have JavaScript and CSS, and interact with a Database. It&#39;s built on Ruby standard libraries and community libraries like <a href="https://sequel.jeremyevans.net/" target="_blank" rel="noreferrer">Sequel</a> and <a href="https://phlex.fun" target="_blank" rel="noreferrer">Phlex</a>.</p><p>Brut&#39;s approach and design are built on three core values:</p><ul><li><strong>Leverage Standards</strong> - The web platform is great, and Brut wants you to use it.</li><li><strong>There&#39;s One Best Way To Do It</strong> - Flexibility leads to chaos.</li><li><strong>Simple over Easy</strong> - Verbose code that can be quickly understood beats impenetrable compact DSLs every day.</li></ul><p>Brut&#39;s abstractions tend to mirror concepts found in the domain of web sites. For example, a browser serves up a web page at a URL. In Brut, that&#39;s called a <em>page</em> and you&#39;d create a subclass of <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a> to implement it.</p><p>Brut tries to avoid abstractions that simply translate existing standards into a more aestheticly pleasing form. You already need to know CSS, HTML, the Web Platform, and SQL, so there&#39;s little to gain by requiring you to learn a different way to use them.</p><h2 id="yes-you-can-build-a-blog-in-15-minutes" tabindex="-1">Yes, You Can Build a Blog in 15 Minutes <a class="header-anchor" href="#yes-you-can-build-a-blog-in-15-minutes" aria-label="Permalink to &quot;Yes, You Can Build a Blog in 15 Minutes&quot;">​</a></h2>',7),o("iframe",{title:"Make a Blog App in 15(ish) minutes With BrutRB - A New Ruby Web Framework",width:"560",height:"315",src:"https://video.hardlimit.com/videos/embed/ae7EMhwjDq9kSH5dqQ9swV",frameborder:"0",allowfullscreen:"",sandbox:"allow-same-origin allow-scripts allow-popups allow-forms"},null,-1),t('<p><a href="https://github.com/thirdtank/blog-demo" target="_blank" rel="noreferrer">Check out the source code</a> if you want to dive right in.</p><h2 id="basic-elements" tabindex="-1">Basic Elements <a class="header-anchor" href="#basic-elements" aria-label="Permalink to &quot;Basic Elements&quot;">​</a></h2><p>Brut organizes its code and behavior around four basic concepts:</p><ul><li><strong>Client</strong> or <em>Client Side</em> is the web browser (or HTTP client). This is where CSS is applied to HTML and where JavaScript is executed. HTTP requests are initiated here.</li><li><strong>Server</strong> or <em>Server Side</em> is where any code not in the browser runs. In Brut, this includes HTML generation, SQL queries, and everything in between.</li><li><strong>Front End</strong> is the code that deals with producing your user interface or HTTP API. A lot of this code runs on the <em>server side</em>, however it exists to provide a user interface of some sort.</li><li><strong>Back End</strong> is the code that deals with everything else, such as accessing a database, executing business logic, or managing background jobs.</li></ul><p><img src="'+i+'" alt="Architectural Overview"></p><ul><li><strong>Visitor</strong> is someone visiting your web site or app.</li><li><strong>Browser</strong> is, well, a web browser</li><li><a href="/pages.html"><strong>Pages</strong></a> generate web pages, which is what happens when a browser&#39;s UI navigates to a URL.</li><li><a href="/forms.html"><strong>Forms</strong></a> are submitted by the browser to the server. In Brut, a form describes the contents of a <code>&lt;form&gt;</code> as well as provides access to the submitted data.</li><li><a href="/handlers.html"><strong>Handlers</strong></a> receive non-GET HTTP requests from the browser, notably form submissions.</li><li><a href="/components.html"><strong>Components</strong></a> generate HTML fragments and are used to generate the HTML of a page or for re-use across pages.</li><li><a href="/javascript.html"><strong>JavaScript</strong></a> and <a href="/assets.html"><strong>Assets</strong></a> (including <a href="/css.html">CSS</a>) are bundled on the server and sent to the client.</li><li><a href="/business-logic.html"><strong>Domain Logic</strong></a> as where your business and domain logic lives and can be implemented however you like.</li><li><a href="/database-access.html"><strong>DB Models</strong></a> are objects that provide access to your database.</li><li><strong>Relational Database</strong> is your database, where data is stored.</li></ul><p>Brut doesn&#39;t prevent the addition of more pieces of infrastructure or code. You can add a Redis cache, a Sidekiq job backend, or integrate with third party APIs.</p><h2 id="brut-is-not-mvc" tabindex="-1">Brut is Not MVC <a class="header-anchor" href="#brut-is-not-mvc" aria-label="Permalink to &quot;Brut is Not MVC&quot;">​</a></h2><p>Brut is <em>not</em> an MVC framework, nor does it use the concept of <em>resources</em> as an abstraction. Although HTTP does include this concept, we find it&#39;s not as useful for managing web apps as it may seem.</p><p>We&#39;ve found that teams often struggle with mapping what everyone else calls pages to resources and HTTP verbs. We also find that the consonance of features, resources, actions, and database tables never materializes. We&#39;re basically not going to debate what the meaning of the <code>DELETE</code> verb on the <code>widgets</code> resource is actually supposed to mean.</p><h2 id="brut-is-hippocratic-licensed" tabindex="-1">Brut is Hippocratic Licensed <a class="header-anchor" href="#brut-is-hippocratic-licensed" aria-label="Permalink to &quot;Brut is Hippocratic Licensed&quot;">​</a></h2><p>It&#39;s important to me that Brut is used to make the world a better place. Please take a note of <a href="https://firstdonoharm.dev/version/3/0/cl-eco-media-my-tal-xuar.txt" target="_blank" rel="noreferrer">its license</a></p>',12)])])}const b=a(n,[["render",l]]);export{g as __pageData,b as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as r,o as s,ag as t,j as o}from"./chunks/framework.C4nOkCZI.js";const i="/assets/OverviewMetro.DUS-5fUZ.png",g=JSON.parse('{"title":"Conceptual Overview","description":"","frontmatter":{},"headers":[],"relativePath":"overview.md","filePath":"overview.md"}'),n={name:"overview.md"};function l(d,e,c,h,u,p){return s(),r("div",null,[...e[0]||(e[0]=[t("",7),o("iframe",{title:"Make a Blog App in 15(ish) minutes With BrutRB - A New Ruby Web Framework",width:"560",height:"315",src:"https://video.hardlimit.com/videos/embed/ae7EMhwjDq9kSH5dqQ9swV",frameborder:"0",allowfullscreen:"",sandbox:"allow-same-origin allow-scripts allow-popups allow-forms"},null,-1),t("",12)])])}const b=a(n,[["render",l]]);export{g as __pageData,b as default};
@@ -1,4 +1,4 @@
1
- import{_ as a,c as s,o as t,ag as i}from"./chunks/framework.1L-BeKqY.js";const k=JSON.parse('{"title":"Pages","description":"","frontmatter":{},"headers":[],"relativePath":"pages.md","filePath":"pages.md"}'),n={name:"pages.md"};function o(l,e,r,h,d,p){return t(),s("div",null,e[0]||(e[0]=[i(`<h1 id="pages" tabindex="-1">Pages <a class="header-anchor" href="#pages" aria-label="Permalink to &quot;Pages&quot;">​</a></h1><p>A core abstraction of Brut is the core concept of the web: the web page.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>To create a web page, you&#39;ll need:</p><ul><li>A <a href="/routes.html">Route</a> using <code>page</code>.</li><li>A class in <code>app/src/front_end/pages/</code> that extends <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>, named <a href="/routes.html#class-naming-conventions">conventionally</a> (though in reality, your page willextend <code>AppPage</code> in <code>app/src/front_end/pages/app_page.rb</code>, which extends <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>).</li><li>[Optional, but recommended] A test in <code>specs/front_end/pages</code>.</li></ul><p>You can create all this with <code>bin/scaffold</code>, which accepts the route you want:</p><div class="language-shell vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> bin/scaffold page /new_widgets</span></span>
1
+ import{_ as a,c as s,o as t,ag as i}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"Pages","description":"","frontmatter":{},"headers":[],"relativePath":"pages.md","filePath":"pages.md"}'),n={name:"pages.md"};function o(l,e,r,h,d,p){return t(),s("div",null,[...e[0]||(e[0]=[i(`<h1 id="pages" tabindex="-1">Pages <a class="header-anchor" href="#pages" aria-label="Permalink to &quot;Pages&quot;">​</a></h1><p>A core abstraction of Brut is the core concept of the web: the web page.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>To create a web page, you&#39;ll need:</p><ul><li>A <a href="/routes.html">Route</a> using <code>page</code>.</li><li>A class in <code>app/src/front_end/pages/</code> that extends <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>, named <a href="/routes.html#class-naming-conventions">conventionally</a> (though in reality, your page willextend <code>AppPage</code> in <code>app/src/front_end/pages/app_page.rb</code>, which extends <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>).</li><li>[Optional, but recommended] A test in <code>specs/front_end/pages</code>.</li></ul><p>You can create all this with <code>bin/scaffold</code>, which accepts the route you want:</p><div class="language-shell vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> bin/scaffold page /new_widgets</span></span>
2
2
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; app/src/front_end/pages/new_widgets_page.rb</span></span>
3
3
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; specs/front_end/pages/new_widgets_page.spec.rb</span></span>
4
4
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; add \`page &quot;/new_widgets&quot;\` to app/src/app.rb</span></span></code></pre></div><p>or</p><div class="language-shell vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">shell</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">&gt;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> bin/scaffold page /widget/:id</span></span>
@@ -42,4 +42,4 @@ import{_ as a,c as s,o as t,ag as i}from"./chunks/framework.1L-BeKqY.js";const k
42
42
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;404&#39;s&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
43
43
  <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">generate_result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
44
44
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_returned_http_status</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">404</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
45
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><ul><li><code>have_redirected_to</code> to check that a redirect happened to the URI you set (see <a href="/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveRedirectedTo</code></a>)</li><li><code>have_returned_http_status</code> to check that a given HTTP response was returned (see <a href="/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveReturnedHttpStatus</code></a>)</li></ul><p>Beyond this, you can use Nokogiri as usual to navigate the DOM that&#39;s generated and make assertions. A few additional matchers to help are:</p><ul><li><code>be_routing_for</code> - expect a URI to be a routing for a certain page or page/parameter combination. See <a href="/api/Brut/SpecSupport/Matchers/BeRoutingFor.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::BeRoutingFor</code></a>.</li><li><code>have_html_attribute</code> - check that a node has an attribute or an attribute with a specific value. See <a href="/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveHTMLAttribute</code></a>.</li><li><code>have_i18n_string</code> - check that a node&#39;s text has a string from your <a href="/i18n.html">I18n</a> configuration. See <a href="/api/Brut/SpecSupport/Matchers/HaveI18nString.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveI18nString</code></a>.</li></ul><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><h3 id="instance-variables-ivars-are-fine" tabindex="-1">Instance variables (ivars) are fine. <a class="header-anchor" href="#instance-variables-ivars-are-fine" aria-label="Permalink to &quot;Instance variables (ivars) are fine.&quot;">​</a></h3><p>Since <code>page_template</code> is a method of your class, it has access to your instance variables (ivars). Feel free to use them directly. Only create <code>attr_reader</code> implementations if a subclass should be expected to override something or you want something lazily evaluated. Make them private. Your page&#39;s API is just the method <code>page_template</code>.</p><h3 id="don-t-set-ivars-in-before-generate" tabindex="-1">Don&#39;t set ivars in <code>before_generate</code> <a class="header-anchor" href="#don-t-set-ivars-in-before-generate" aria-label="Permalink to &quot;Don&#39;t set ivars in \`before_generate\`&quot;">​</a></h3><p>It&#39;s Ruby and you can do whatever you want, but your page class will be easier to understand and test if you set up necessary state in your initializer. Memoization is fine, but don&#39;t have your <code>before_generate</code> set up additional state if you can avoid it. As we&#39;ll see below, you won&#39;t need to use <code>before_generate</code> as a failsafe check on authorization.</p><h3 id="leverage-keyword-injection" tabindex="-1">Leverage Keyword Injection <a class="header-anchor" href="#leverage-keyword-injection" aria-label="Permalink to &quot;Leverage Keyword Injection&quot;">​</a></h3><p>The list of available data for injection above will always be available to your page, with the exception of query string parameters. The real power comes when you learn how to <a href="/keyword-injection.html#injecting-custom-data">inject your own data</a> into the request context.</p><p>A great example of this is in the <a href="/recipes/authentication.html">recipe for keywords and auth</a>, which results in a much simpler and less error-prone way to prevent unauthorized access to pages when compared to how you might do it in Rails.</p><h3 id="in-tests-it-s-fine-to-locate-elements-via-css-selectors" tabindex="-1">In Tests, It&#39;s Fine to Locate Elements Via CSS Selectors <a class="header-anchor" href="#in-tests-it-s-fine-to-locate-elements-via-css-selectors" aria-label="Permalink to &quot;In Tests, It&#39;s Fine to Locate Elements Via CSS Selectors&quot;">​</a></h3><p>Your page&#39;s job is to produce HTML. To check if it&#39;s doing that, it makes sense to manipulate that HTML using standard, battle-tested techniques like CSS selectors. This creates consonance between your in-browser debugging and your test suite.</p><p>It also makes it much more obvious what&#39;s wrong if something is not where you expect it to be.</p><h3 id="that-said-avoid-test-specific-attributes-or-classes" tabindex="-1">That Said, Avoid Test-Specific Attributes or Classes <a class="header-anchor" href="#that-said-avoid-test-specific-attributes-or-classes" aria-label="Permalink to &quot;That Said, Avoid Test-Specific Attributes or Classes&quot;">​</a></h3><p>When you have a lot of <code>&lt;div&gt;</code> elements, it can be tempting to use attributes like <code>data-testid</code> on the elements you want to find in your tests. You can often avoid this if you use semantic markup and proper ARIA roles. For example, a Flash message is likely something you&#39;d put in a <code>role=&quot;status&quot;</code> or <code>role=&quot;alert&quot;</code>, so you don&#39;t need <code>data-flash</code> or <code>class=&quot;flash&quot;</code> in order to find it in a test.</p><p>Custom Elements can also be helpful here, as that may be how you choose to manage your client-side behavior.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 4, 2025</em></p><h3 id="page-internal-api" tabindex="-1">Page Internal API <a class="header-anchor" href="#page-internal-api" aria-label="Permalink to &quot;Page Internal API&quot;">​</a></h3><p>A Page&#39;s core API is the method <code>handle!</code>, which can return an HTML-safe string, <code>URI</code>, or Rack response. Developers should avoid overriding this method, as it also handles the logic related to calling <code>before_generate</code> as well as the logic required to make layouts work.</p><p>This is why we recommend using <code>Brut::SpecSupport::ComponentSupport#generate_and_parse</code> or <code>Brut::SpecSupport::ComponentSupport#generate_result</code> in a tests. <em>They</em> call <code>handle!</code>, thus ensuring your <code>before_generate</code> method will be called and that your page class will behave in a test the way it would in production.</p><h3 id="layouts" tabindex="-1">Layouts <a class="header-anchor" href="#layouts" aria-label="Permalink to &quot;Layouts&quot;">​</a></h3><p>Pages do not have to have a layout. You can override Phlex&#39;s <code>view_template</code> and produce HTML that will not be wrapped in any Layout. It may be a better idea to create a <code>BlankLayout</code> class to avoid this, but it&#39;s up to you.</p><h3 id="helpers-in-templates" tabindex="-1">Helpers in Templates <a class="header-anchor" href="#helpers-in-templates" aria-label="Permalink to &quot;Helpers in Templates&quot;">​</a></h3><p><a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a> is a subclass of <a href="/api/Brut/FrontEnd/Component.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Component</code></a>, so all your pages will have access to the helpers included there. This is how, for example, <code>t</code> can be called to perform translations.</p><p>Note that Brut does <em>not</em> include <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a> (pluralized). You can include that in <code>AppPage</code> to access Brut&#39;s builtin components as a Phlex kit.</p><h3 id="so-you-don-t-like-phlex" tabindex="-1">So You Don&#39;t Like Phlex? <a class="header-anchor" href="#so-you-don-t-like-phlex" aria-label="Permalink to &quot;So You Don&#39;t Like Phlex?&quot;">​</a></h3><p>Brut did initially use ERB, but the initial Brut-powered apps ended up having an all-too-common mess of HTML, Ruby, and angle brackets. It really sucked. Phlex seems pretty solid and is a very lightweight abstraction over HTML. It keeps everything in Ruby, but still maintains consonance to what you see in your browser.</p><p>Support for ERB, Slim, or HAML, is not planned ever.</p>`,77)]))}const u=a(n,[["render",o]]);export{k as __pageData,u as default};
45
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><ul><li><code>have_redirected_to</code> to check that a redirect happened to the URI you set (see <a href="/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveRedirectedTo</code></a>)</li><li><code>have_returned_http_status</code> to check that a given HTTP response was returned (see <a href="/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveReturnedHttpStatus</code></a>)</li></ul><p>Beyond this, you can use Nokogiri as usual to navigate the DOM that&#39;s generated and make assertions. A few additional matchers to help are:</p><ul><li><code>be_routing_for</code> - expect a URI to be a routing for a certain page or page/parameter combination. See <a href="/api/Brut/SpecSupport/Matchers/BeRoutingFor.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::BeRoutingFor</code></a>.</li><li><code>have_html_attribute</code> - check that a node has an attribute or an attribute with a specific value. See <a href="/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveHTMLAttribute</code></a>.</li><li><code>have_i18n_string</code> - check that a node&#39;s text has a string from your <a href="/i18n.html">I18n</a> configuration. See <a href="/api/Brut/SpecSupport/Matchers/HaveI18nString.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveI18nString</code></a>.</li></ul><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><h3 id="instance-variables-ivars-are-fine" tabindex="-1">Instance variables (ivars) are fine. <a class="header-anchor" href="#instance-variables-ivars-are-fine" aria-label="Permalink to &quot;Instance variables (ivars) are fine.&quot;">​</a></h3><p>Since <code>page_template</code> is a method of your class, it has access to your instance variables (ivars). Feel free to use them directly. Only create <code>attr_reader</code> implementations if a subclass should be expected to override something or you want something lazily evaluated. Make them private. Your page&#39;s API is just the method <code>page_template</code>.</p><h3 id="don-t-set-ivars-in-before-generate" tabindex="-1">Don&#39;t set ivars in <code>before_generate</code> <a class="header-anchor" href="#don-t-set-ivars-in-before-generate" aria-label="Permalink to &quot;Don&#39;t set ivars in \`before_generate\`&quot;">​</a></h3><p>It&#39;s Ruby and you can do whatever you want, but your page class will be easier to understand and test if you set up necessary state in your initializer. Memoization is fine, but don&#39;t have your <code>before_generate</code> set up additional state if you can avoid it. As we&#39;ll see below, you won&#39;t need to use <code>before_generate</code> as a failsafe check on authorization.</p><h3 id="leverage-keyword-injection" tabindex="-1">Leverage Keyword Injection <a class="header-anchor" href="#leverage-keyword-injection" aria-label="Permalink to &quot;Leverage Keyword Injection&quot;">​</a></h3><p>The list of available data for injection above will always be available to your page, with the exception of query string parameters. The real power comes when you learn how to <a href="/keyword-injection.html#injecting-custom-data">inject your own data</a> into the request context.</p><p>A great example of this is in the <a href="/recipes/authentication.html">recipe for keywords and auth</a>, which results in a much simpler and less error-prone way to prevent unauthorized access to pages when compared to how you might do it in Rails.</p><h3 id="in-tests-it-s-fine-to-locate-elements-via-css-selectors" tabindex="-1">In Tests, It&#39;s Fine to Locate Elements Via CSS Selectors <a class="header-anchor" href="#in-tests-it-s-fine-to-locate-elements-via-css-selectors" aria-label="Permalink to &quot;In Tests, It&#39;s Fine to Locate Elements Via CSS Selectors&quot;">​</a></h3><p>Your page&#39;s job is to produce HTML. To check if it&#39;s doing that, it makes sense to manipulate that HTML using standard, battle-tested techniques like CSS selectors. This creates consonance between your in-browser debugging and your test suite.</p><p>It also makes it much more obvious what&#39;s wrong if something is not where you expect it to be.</p><h3 id="that-said-avoid-test-specific-attributes-or-classes" tabindex="-1">That Said, Avoid Test-Specific Attributes or Classes <a class="header-anchor" href="#that-said-avoid-test-specific-attributes-or-classes" aria-label="Permalink to &quot;That Said, Avoid Test-Specific Attributes or Classes&quot;">​</a></h3><p>When you have a lot of <code>&lt;div&gt;</code> elements, it can be tempting to use attributes like <code>data-testid</code> on the elements you want to find in your tests. You can often avoid this if you use semantic markup and proper ARIA roles. For example, a Flash message is likely something you&#39;d put in a <code>role=&quot;status&quot;</code> or <code>role=&quot;alert&quot;</code>, so you don&#39;t need <code>data-flash</code> or <code>class=&quot;flash&quot;</code> in order to find it in a test.</p><p>Custom Elements can also be helpful here, as that may be how you choose to manage your client-side behavior.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 4, 2025</em></p><h3 id="page-internal-api" tabindex="-1">Page Internal API <a class="header-anchor" href="#page-internal-api" aria-label="Permalink to &quot;Page Internal API&quot;">​</a></h3><p>A Page&#39;s core API is the method <code>handle!</code>, which can return an HTML-safe string, <code>URI</code>, or Rack response. Developers should avoid overriding this method, as it also handles the logic related to calling <code>before_generate</code> as well as the logic required to make layouts work.</p><p>This is why we recommend using <code>Brut::SpecSupport::ComponentSupport#generate_and_parse</code> or <code>Brut::SpecSupport::ComponentSupport#generate_result</code> in a tests. <em>They</em> call <code>handle!</code>, thus ensuring your <code>before_generate</code> method will be called and that your page class will behave in a test the way it would in production.</p><h3 id="layouts" tabindex="-1">Layouts <a class="header-anchor" href="#layouts" aria-label="Permalink to &quot;Layouts&quot;">​</a></h3><p>Pages do not have to have a layout. You can override Phlex&#39;s <code>view_template</code> and produce HTML that will not be wrapped in any Layout. It may be a better idea to create a <code>BlankLayout</code> class to avoid this, but it&#39;s up to you.</p><h3 id="helpers-in-templates" tabindex="-1">Helpers in Templates <a class="header-anchor" href="#helpers-in-templates" aria-label="Permalink to &quot;Helpers in Templates&quot;">​</a></h3><p><a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a> is a subclass of <a href="/api/Brut/FrontEnd/Component.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Component</code></a>, so all your pages will have access to the helpers included there. This is how, for example, <code>t</code> can be called to perform translations.</p><p>Note that Brut does <em>not</em> include <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a> (pluralized). You can include that in <code>AppPage</code> to access Brut&#39;s builtin components as a Phlex kit.</p><h3 id="so-you-don-t-like-phlex" tabindex="-1">So You Don&#39;t Like Phlex? <a class="header-anchor" href="#so-you-don-t-like-phlex" aria-label="Permalink to &quot;So You Don&#39;t Like Phlex?&quot;">​</a></h3><p>Brut did initially use ERB, but the initial Brut-powered apps ended up having an all-too-common mess of HTML, Ruby, and angle brackets. It really sucked. Phlex seems pretty solid and is a very lightweight abstraction over HTML. It keeps everything in Ruby, but still maintains consonance to what you see in your browser.</p><p>Support for ERB, Slim, or HAML, is not planned ever.</p>`,77)])])}const u=a(n,[["render",o]]);export{k as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as s,o as t,ag as i}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"Pages","description":"","frontmatter":{},"headers":[],"relativePath":"pages.md","filePath":"pages.md"}'),n={name:"pages.md"};function o(l,e,r,h,d,p){return t(),s("div",null,[...e[0]||(e[0]=[i("",77)])])}const u=a(n,[["render",o]]);export{k as __pageData,u as default};
@@ -1,4 +1,4 @@
1
- import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const o=JSON.parse('{"title":"Alternate Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/alternate-layouts.md","filePath":"recipes/alternate-layouts.md"}'),l={name:"recipes/alternate-layouts.md"};function e(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="alternate-layouts" tabindex="-1">Alternate Layouts <a class="header-anchor" href="#alternate-layouts" aria-label="Permalink to &quot;Alternate Layouts&quot;">​</a></h1><p>To create an alternate layout, your page can override <code>layout</code> to return a string. That string will be camel-cased and preped to <code>Layout</code> to form a class that is expected to exist and provide the layout. That class must extend <a href="/api/Brut/FrontEnd/Layout.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Layout</code></a>.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> MyOtherPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const o=JSON.parse('{"title":"Alternate Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/alternate-layouts.md","filePath":"recipes/alternate-layouts.md"}'),l={name:"recipes/alternate-layouts.md"};function e(h,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[t(`<h1 id="alternate-layouts" tabindex="-1">Alternate Layouts <a class="header-anchor" href="#alternate-layouts" aria-label="Permalink to &quot;Alternate Layouts&quot;">​</a></h1><p>To create an alternate layout, your page can override <code>layout</code> to return a string. That string will be camel-cased and preped to <code>Layout</code> to form a class that is expected to exist and provide the layout. That class must extend <a href="/api/Brut/FrontEnd/Layout.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Layout</code></a>.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> MyOtherPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
2
2
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> layout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;other_design&quot;</span></span>
3
3
  <span class="line"></span>
4
4
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
@@ -19,4 +19,4 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const o
19
19
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
20
20
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
21
21
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
22
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,3)]))}const E=i(l,[["render",e]]);export{o as __pageData,E as default};
22
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,3)])])}const E=i(l,[["render",e]]);export{o as __pageData,E as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const o=JSON.parse('{"title":"Alternate Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/alternate-layouts.md","filePath":"recipes/alternate-layouts.md"}'),l={name:"recipes/alternate-layouts.md"};function e(h,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[t("",3)])])}const E=i(l,[["render",e]]);export{o as __pageData,E as default};
@@ -1,4 +1,4 @@
1
- import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const o=JSON.parse('{"title":"Authentication Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/authentication.md","filePath":"recipes/authentication.md"}'),h={name:"recipes/authentication.md"};function l(e,s,p,k,d,r){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="authentication-example" tabindex="-1">Authentication Example <a class="header-anchor" href="#authentication-example" aria-label="Permalink to &quot;Authentication Example&quot;">​</a></h1><p>It&#39;s impossible to account for all types of authentication you may want to use, but this recipe will demonstrate all the moving parts:</p><ul><li>How to require authentication for some pages</li><li>How to design pages that require authentication</li><li>How to manage the signed-in user in code</li></ul><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to &quot;Feature&quot;">​</a></h2><ul><li>Visitors can log in with an email, that is assumed to have been inserted previously (no passwords or signup, just to simplify the recipe)</li><li>Visitors can access the home page without logging in</li><li>Visitors cannot access the dashboard page without logging in</li></ul><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><h3 id="set-up-database-and-seed-data" tabindex="-1">Set up Database and Seed Data <a class="header-anchor" href="#set-up-database-and-seed-data" aria-label="Permalink to &quot;Set up Database and Seed Data&quot;">​</a></h3><p>First, we&#39;ll make a database table called <code>accounts</code> that will have an email field and a password hash field.</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db new-migration accounts</span></span></code></pre></div><p>This will create a file in <code>app/src/back_end/data_models/migrations</code>. We&#39;ll edit it to create a new table called <code>accounts</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">migration</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const o=JSON.parse('{"title":"Authentication Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/authentication.md","filePath":"recipes/authentication.md"}'),h={name:"recipes/authentication.md"};function l(e,s,p,k,d,r){return n(),a("div",null,[...s[0]||(s[0]=[t(`<h1 id="authentication-example" tabindex="-1">Authentication Example <a class="header-anchor" href="#authentication-example" aria-label="Permalink to &quot;Authentication Example&quot;">​</a></h1><p>It&#39;s impossible to account for all types of authentication you may want to use, but this recipe will demonstrate all the moving parts:</p><ul><li>How to require authentication for some pages</li><li>How to design pages that require authentication</li><li>How to manage the signed-in user in code</li></ul><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to &quot;Feature&quot;">​</a></h2><ul><li>Visitors can log in with an email, that is assumed to have been inserted previously (no passwords or signup, just to simplify the recipe)</li><li>Visitors can access the home page without logging in</li><li>Visitors cannot access the dashboard page without logging in</li></ul><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><h3 id="set-up-database-and-seed-data" tabindex="-1">Set up Database and Seed Data <a class="header-anchor" href="#set-up-database-and-seed-data" aria-label="Permalink to &quot;Set up Database and Seed Data&quot;">​</a></h3><p>First, we&#39;ll make a database table called <code>accounts</code> that will have an email field and a password hash field.</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db new-migration accounts</span></span></code></pre></div><p>This will create a file in <code>app/src/back_end/data_models/migrations</code>. We&#39;ll edit it to create a new table called <code>accounts</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">migration</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
2
2
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
3
3
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:accounts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;People or systems who can access this system&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">external_id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
4
4
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">unique:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
@@ -154,4 +154,4 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const o
154
154
  <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> before_action </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:require_login!</span></span>
155
155
  <span class="line"></span>
156
156
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
157
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This could be shared in a parent page, but you essentially have to remember to do this on every page that requires login (or do the opposite - allow specific pages to be accessed without logging in).</p><p>In Rails, this is a good practice, because even though your views won&#39;t route a logged-out visitor to a logged-in page, URL hacking or bugs could result in an attempt to do so. You need the failsafe.</p><p>In Brut, the very definition of the page&#39;s class includes the requirement for the <code>current_account</code>. The page cannot be instantiated without it.</p><p>Thus, there is no need for a failsafe. <code>SetupCurrentAccount</code> handles checking the routes, and that&#39;s it. If someone hacks a URL or a bug in the code sends a logged-out visitor to the dashboard page, Brut literally cannot handle the request, since the <code>current_account</code> will be missing.</p>`,58)]))}const E=i(h,[["render",l]]);export{o as __pageData,E as default};
157
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This could be shared in a parent page, but you essentially have to remember to do this on every page that requires login (or do the opposite - allow specific pages to be accessed without logging in).</p><p>In Rails, this is a good practice, because even though your views won&#39;t route a logged-out visitor to a logged-in page, URL hacking or bugs could result in an attempt to do so. You need the failsafe.</p><p>In Brut, the very definition of the page&#39;s class includes the requirement for the <code>current_account</code>. The page cannot be instantiated without it.</p><p>Thus, there is no need for a failsafe. <code>SetupCurrentAccount</code> handles checking the routes, and that&#39;s it. If someone hacks a URL or a bug in the code sends a logged-out visitor to the dashboard page, Brut literally cannot handle the request, since the <code>current_account</code> will be missing.</p>`,58)])])}const E=i(h,[["render",l]]);export{o as __pageData,E as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.C4nOkCZI.js";const o=JSON.parse('{"title":"Authentication Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/authentication.md","filePath":"recipes/authentication.md"}'),h={name:"recipes/authentication.md"};function l(e,s,p,k,d,r){return n(),a("div",null,[...s[0]||(s[0]=[t("",58)])])}const E=i(h,[["render",l]]);export{o as __pageData,E as default};
@@ -1,4 +1,4 @@
1
- import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Custom Flash Class","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/custom-flash.md","filePath":"recipes/custom-flash.md"}'),l={name:"recipes/custom-flash.md"};function p(e,s,t,k,d,r){return n(),a("div",null,s[0]||(s[0]=[h(`<h1 id="custom-flash-class" tabindex="-1">Custom Flash Class <a class="header-anchor" href="#custom-flash-class" aria-label="Permalink to &quot;Custom Flash Class&quot;">​</a></h1><p>If you want to have a more sophisticated <a href="/flash-and-session.html">Flash</a>, you can do this by overriding Brut&#39;s <a href="/configuration.html">configuration</a>.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><p>First, create your new class in <code>app/support/app_flash.rb</code>. You can implement your new methods using <code>[]</code> and <code>[]=</code>.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppFlash</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Flash</span></span>
1
+ import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Custom Flash Class","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/custom-flash.md","filePath":"recipes/custom-flash.md"}'),l={name:"recipes/custom-flash.md"};function p(e,s,t,k,d,r){return n(),a("div",null,[...s[0]||(s[0]=[h(`<h1 id="custom-flash-class" tabindex="-1">Custom Flash Class <a class="header-anchor" href="#custom-flash-class" aria-label="Permalink to &quot;Custom Flash Class&quot;">​</a></h1><p>If you want to have a more sophisticated <a href="/flash-and-session.html">Flash</a>, you can do this by overriding Brut&#39;s <a href="/configuration.html">configuration</a>.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><p>First, create your new class in <code>app/support/app_flash.rb</code>. You can implement your new methods using <code>[]</code> and <code>[]=</code>.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppFlash</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Flash</span></span>
2
2
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> debug</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> self[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
3
3
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> debug?</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">self.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">debug</span></span>
4
4
  <span class="line"></span>
@@ -23,4 +23,4 @@ import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.1L-BeKqY.js";const c
23
23
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> aside { @flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
24
24
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
25
25
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
26
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,9)]))}const o=i(l,[["render",p]]);export{c as __pageData,o as default};
26
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,9)])])}const o=i(l,[["render",p]]);export{c as __pageData,o as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as a,o as n,ag as h}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Custom Flash Class","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/custom-flash.md","filePath":"recipes/custom-flash.md"}'),l={name:"recipes/custom-flash.md"};function p(e,s,t,k,d,r){return n(),a("div",null,[...s[0]||(s[0]=[h("",9)])])}const o=i(l,[["render",p]]);export{c as __pageData,o as default};