Skip to content

Proposal: Migrate to Templ #34631

Open
Open
@philip-peterson

Description

@philip-peterson

Feature Description

Hi all, I have investigated the use of Templ as a replacement for the classic Go text/template and html/template packages as suggested in 31379.

To do this, a Proof-of-Concept was implemented of the /post-install page, using Templ. This involved migrating the existing templates/post-install.tmpl as well as base/header.tmpl and base/footer.tmpl.

A branch is available for you to review the changes yourself, and the findings are summarized below.

Why?

The reason for wanting to replace text/template is that the templates have grown in complexity and have lagged behind in structural maintenance.

For example, because Pull Requests and Issues are the same model under the hood, many templates have different behavior triggered by branching off .IsPull -- there are dozens of checks for this. The more ideal scenario would be having two different templates for Pull Requests and Issues, each of which may share some elements (reusable sub-components) but which have distinctly different layouts. So componentization is one desirable characteristic.

Another is auditability. Because text/template templates are dynamic, they require running them to uncover many issues. Refactors are therefore uncertain endeavors and generally avoided. There is more on this in the sections below.

Running the Branch

Branch link: https://github.com/go-gitea/gitea/compare/main...philip-peterson:gitea:templ?expand=1

To run, make sure in app.ini you have

[security]
INSTALL_LOCK = false

Then, simply invoke TAGS="bindata sqlite sqlite_unlock_notify" make watch-backend. It should open a browser window, and then you can navigate to http://localhost:7331/post-install . (Note that port 7331 is a proxy with hot reloading that Templ provides which proxies to the usual port 3000).

Findings

Differences with text/template

  • When transcluding templates, structure must be tree-like, i.e. the header import which imports a but not a , is not allowed in Templ. An entire pair, e.g. pair must be contained within a single template, although within the tag you can include {children…} which allows for composition.
  • Non-closed tags are not allowed, so every opening tag must end in /> or have a matching closing tag.
  • Templ templates directly import packages, since the target of a templ template is Go code. Any referenced symbol or package name must be imported.
  • (minor) Templ templates are referred to via symbol name instead of filename.

Issues / Drawbacks

  • While with most small text and attribute changes, Templ can hot reload (and does so very quickly), larger changes including changing or moving Go logic and even some larger HTML-only operations require a full restart. Templ doesn’t do a great job at telling you you need to restart, you just have to know. But I think if you’re just tweaking CSS classes it should reload instantly.
  • Templ does support Air, but I tried it out and the hot reloading was very slow.
  • Values injected (into attributes and other places) could use a better sanitization story. There is however support for HTML injection protection (one must use @templ.Raw to disable it).
  • Templ seems to assign special meaning to the variable ctx, so in order to use Gitea contexts, we may have to use a different name like gCtx. I did not investigate the full extent to why this is required.
  • Because context variables come in untyped, they must (usually) be cast early to an explicit annotation of their type. This could actually be a benefit however though, as it allows a fail-fast mechanism for variables whose types are not what we expect, and it also adds type checking for variable obtained via context is passed into a function.
  • Templ templates compile to Go code, which include imports to other part of Gitea. If we get unlucky, this may mean cyclic import errors to contend with. I am not sure where these would pop up, though, since they did not pose an issue in the POC.

Benefits

  • Go code can be defined inline instead of needing to rely on helpers defined elsewhere. This makes the scope of local helpers easier to observe.
  • Templates accepting children makes the templates much more single-purpose.
  • Template-dicts (e.g. {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}), which were untyped, are no longer necessary.
  • Missing template references will fail to compile, along with many other invalid reference errors.
  • Composition is much easier and multiple sub-templates can be defined in a single file. Also, parameters to each template are typed (though they can be pulled from the context).
  • Because the Go code can be defined inline, the number of things a template can do is far expanded, for example @partial(ctx, templateName) can be invoked which includes an old text/template template within a new Templ template.
  • Provenance of variables is clearer – they can more easily visually be traced back to a passed-in argument vs. a global context variable. There is no concept of a scope separate from Go’s own concept of scoping. Function calls also look like Go function calls, so the difference between a variable reference and a function instantiation is obvious.
  • Reduced dependence on context — because arguments are explicitly declared and typed, they can be passed directly to sub-templates in a type-safe manner avoiding the reliance on context to some extent.

Security

As far as security, I do believe that is the main risk for using this library.

These were some of the largest users of the repo I could find, perhaps some of them would also have an interest in performing a security audit:

7.9k stars - https://github.com/TecharoHQ/anubis
2.2k stars - https://github.com/starfederation/datastar
1.1k stars - https://github.com/jovandeginste/workout-tracker
1.1k stars - https://github.com/anthdm/superkit
1k stars - https://github.com/almeidapaulopt/tsdproxy
855 stars - https://github.com/damongolding/immich-kiosk
659 stars - https://github.com/TomDoesTech/GOTTH
400 stars - https://github.com/leomorpho/goship
270 stars - https://github.com/yumenaka/comigo
204 stars - https://github.com/JonasHiltl/openchangelog
185 stars - https://github.com/blackfyre/wga

Compared to Gomponents

For context, previously I had created another branch to prove the templates could be rewritten using Gomponents.

In comparison to Gomponents, to be honest, for development (not reading) I still prefer Gomponents since the level of abstraction is not as high. The syntax highlighting story is also simpler, because the code is just Go, when using Gomponents. With Templ, the hot reload is nice (Gomponents does not support hot reload), but I am not sure it outweighs the package's complexity. If it were made more reliable, this would be less of a concern. For reading however, Templ is the obvious winner as it is "just HTML" or close to it.

Summary and Next Steps

In summary, I think from what is known, Templ is an alright library. It's certainly a structural improvement on text/template and html/template. It is very satisfying to break up an existing file, and nice that there is a path forward for gradual adoption via @partial.

The watcher tool could use some upstream patches to improve its reliability and level of feedback, but usually if there is an issue, restarting the tool either solves the issue or provides the feedback needed to resolve it.

Next steps I think would be a security audit as well as development of best practices regarding escaping values and a performance stress test.

See also

https://github.com/axzilla/templui
https://github.com/slinso/goTemplateBenchmark

Thoughts?

I am interested if you have any feedback or thoughts?

Screenshots

Image

Image above is /post-install running using Templ.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/proposalThe new feature has not been accepted yet but needs to be discussed first.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions