SkillAgentSearch skills...

Element

Native Go (golang) HTML generator - template eliminator

Install / Use

/learn @rohanthewiz/Element
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Simple HTML generator

Forget the complexity of frontend frameworks and templates to generate decent HTML pages! Element generates HTML nicely, and simply from Go code. Everything is pure compiled Go without reflection, annotations or weird rules. This has been in use in production for several years.

  • This library includes AI-agent documentation at ai_docs/SKILL.md.
  • Can we make this a standard, so there is one, easily discoverable, source of truth for a library's SKILL?

Benefits

  1. You never leave the safety, speed, and familiarity of Go
    • The full power of Go is at your finger tips
    • Compiles single-pass with the rest of your Go program -- no weird annotations or build steps
    • You don't have to learn some half-baked templating language
    • You rip at Go speed the whole time
  2. Server-side by nature -- not an after thought
  3. Zero dependencies, (yep, no Node and friends), so you don't add vulnerabilities and complexities into your critical projects!
  4. Buffer pools for super-high traffic situations
  5. Easy to use with Go intellisense, and natural nesting paralleling the HTML tree structure

Usage

	b := element.NewBuilder()

	// Body(), like most builder methods, writes the *opening* tag to the underlying string builder
	// and is terminated with R(), which allows children to be rendered, then any closing tag appended,
	// or could be terminated with T(args...string), if the children are all literal text.
	b.Body().R(
		// DivClass is a convenience method for outputting and element with at least a "class" attribute
		b.DivClass("container").R( // -> Creates `<div class="container">...{span and paragraph children} ...</div>`
			b.Span().T("Some text"), // -> Adds "<span>Some text</span>"

			b.P().R( // -> Adds<p><a href="https://example.com">Example.com</a></p>"
				b.A("href", "http://example.com").T("Example.com"),
			), // ending </p> tag
		), // ending </div> tag
	) // </body>

	b.String()
	//-> <body><div class="container"><span>Some text</span><p><a href="http://example.com">Example.com</a></p></div></body>

See the full examples in the examples folder.

How it works

  • Element maintains an underlying bytes.Buffer, to which it appends elements as you go.
  • When you call b.P() an opening paragraph tag is immediately added to the internal buffer.
  • b.P() returns an element.Element.
  • Elements may have children, so we don't render their closing tags as yet. This is where R() comes in.
  • R() causes the calling function (of the element) to wait until all/any children (lexically, arguments) are resolved (rendered). before calling close() on the parent element, rendering it's closing tag.
  • In other words, element leverages the natural order of function and argument execution (a tree, AST) to properly layout HTML elements (also a tree).
  • Element therefore, is natural Go, not a templating or pseudo language shimmed in, but pure, one-shot compiled Go!
  • Also, as everything is written in a single pass with very little memory allocation, it runs at the full speed of Go!

Core principles of Element

  • (See also the SKILL.md in ai_docs/SKILL.md)
  • Element takes a code-first approach to producing HTML.
  • First we get a builder from Element: b := element.NewBuilder().
  • Note: Builder will render element tags to an internal string builder.
  • Then we use the builder to first render the opening tag of the element internally, and return the Element. b.P("id", "special") // -> Element
  • The element will then close in a few possible ways:
    • Most often we will use the Element method R() to
      • allow any children to render b.P("id", "special").R(b.Span().T("We are saying something"))
      • then close out the current element by writing its closing tag (if any) to the internal string builder
    • If the children of the current tag will be purely literal text, it is optimal to use the method T() to
      • allow the pure text children to render. Example: b.P("id", "special").T("one", "two", "three")
      • then close out the current element by writing its closing tag (if any) to the internal string builder
    • If the children of the current tag will be a single formatted piece text, we should use the method F() to
      • allow the formatted text to render similiar to fmt.Sprintf(). Example: b.P("id", "special").F("There are %d items", count)
      • then close out the current element by writing its closing tag (if any) to the internal string builder
  • For elements that have no children prefer to close with R(), rather than T() or T("")
  • Prefer b.T() over b.Text() since b.Text() is being deprecated.
  • When rendering elements, where we include a "class" attribute make sure to use the corresponding builder function ending in "Class".
    • Example: use b.PClass("intro") instead of b.P("class", "intro")
  • When we are done building elements we get the fully rendered HTML with b.String()

Things to Note

  • The actual values returned by children elements are ignored.
  • R() receive arguments of any type, but they are discarded
  • T() like R() can terminate an opened element, but T() is used when the children are literal text only.
  • F() can also terminate an opened element with a single formatted string. Example: b.Span().F("The value is: %0.1f", value)
  • In debug mode, we do peek at the arguments passed to R() to help identify any issue in children elements.

Examples

Building with Multiple Functions

Simply pass the builder around (it is a pointer), and create elements in the desired order. Again, things are written immediately to the builder's buffer.

If you are calling directly from a render tree, as in the example below, you will need to return something to satisfy the calling function (in this case the container div's R()), but the calling function doesn't do anything with its arguments, they are there just to establish calling order.

package main

func main() {
    b := element.B()
    
    b.DivClass("container").R(
        b.H2().T("Section 1"),
        generateList(b),
    )

    fmt.Println(b.Pretty())
    /* // Output:
    <div class="container">
      <h2>Section 1</h2>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
    </div>
    */
}

func generateList(b *element.Builder) (x any) { // we don't care about the return
	// Everything is rendered in the builder immediately!
	b.Ul().R(
		b.Li().T("Item 1"),
		b.Li().T("Item 2"),
	)
	return
}

A Full Example

We use short method names and some aliases to keep the code as unobtrusive as possible. See the examples folder https://github.com/rohanthewiz/element/tree/master/examples for full, ready-to-compile examples.

package main

import (
	"log"
	"strconv"

	"github.com/rohanthewiz/element"
	"github.com/rohanthewiz/rweb"
	"github.com/rohanthewiz/serr"
)

func main() {
	s := rweb.NewServer(rweb.ServerOptions{
		Address: ":8000",
		Verbose: true,
	})

	s.Use(rweb.RequestInfo) // Stats Middleware

	s.Get("/", rootHandler)

	s.Get("/other-page", otherPageHandler)

	// Set debug mode, then go to the page you want to check, refresh it,
	// then go to /debug/show to see any issues
	s.Get("/debug/set", func(c rweb.Context) error {
		element.DebugSet()
		return c.WriteHTML("<h3>Debug mode set.</h3> <a href='/'>Home</a>")
	})

	s.Get("/debug/show", func(c rweb.Context) error {
		err := c.WriteHTML(element.DebugShow())
		return err
	})

	s.Get("/debug/clear", func(c rweb.Context) error {
		element.DebugClear()
		return c.WriteHTML("<h3>Debug mode is off.</h3> <a href='/'>Home</a>")
	})

	// Run Server
	log.Fatal(s.Run())
}

func rootHandler(c rweb.Context) error {
	animals := []string{"cat", "mouse", "dog"}
	colors := []string{"red", "blue", "green", "indigo", "violet"}
	err := c.WriteHTML(generateHTML(animals, colors))
	if err != nil {
		return serr.Wrap(err)
	}
	return nil
}

// SelectComponent is an example of a component
// note that a component is anything that has a Render method
// taking an `*element.Builder` and returning `any`
type SelectComponent struct {
	Selected string
	Items    []string
}

// Render method signature matches the element.Component interface
func (s SelectComponent) Render(b *element.Builder) (x any) {
	b.Select().R(
		func() (x any) {
			for _, color := range s.Items {
				params := []string{"value", color}
				if color == s.Selected {
					params = append(params, "selected", "selected")
				}
				b.Option(params...).T(color)
			}
			return
		}(),
	)
	return
}

func generateHTML(animals []string, colors []string) string {
	b := element.NewBuilder()

	selComp := SelectComponent{Selected: "blue", Items: colors}

	b.Html().R(
		b.Head().R(
			b.Style().T(`
                #page-container {
                    padding: 4rem; height: 100vh; background-color: rgb(232, 230, 228);
                }
                .intro {
                    font-style: italic; font-size: 0.9rem; padding-left: 3em;
                }
                .highlight {
                    background-color: yellow;
                }
                .footer {
                    text-align: center; font-size: 0.8rem; border-top: 1px solid #ccc; padding: 1em;
                }`,
			),
		),
		b.Body().R(
			b.Div("id", "page-container").R(
				b.H1().T("This is my heading"),
				b.DivClass("intro").R(), // this should not show any issues
				b.Div("class", "intro", "unpaired").R( // testing bad pairs
					b.P().R(
						b.T("I've got plenty to say here "),
						b.SpanClass("highlight").R(
							b.T("important phrase!", " More intro text"),
						),
					),
				),
				b.P().R(
					b.T("ABC Company"),
					b.Br(), // single tags don't need to call `.R()` or `.T()`, but no harm in calling `.R()` on single tags
					b.Wrap(func() {
						out := ""
						for i := 0; i < 10; i++ {
							if i > 0 {
								out += ","
							}
							out += strconv.Itoa(i)
						}
						b.T(out)
				
View on GitHub
GitHub Stars16
CategoryDevelopment
Updated1mo ago
Forks0

Languages

Go

Security Score

75/100

Audited on Mar 8, 2026

No findings