pages

package module
v0.0.0-...-c8449d3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2024 License: BSD-3-Clause, MIT Imports: 18 Imported by: 0

README

go-pages

go-pages is a server-side HTML component rendering engine designed to bring the ease and flexibility of Web component development to Go projects.

It consists of a template engine for rendering HTML components (.chtml files) and a file-based router for serving components based on URL paths.

Goals

  • Template engine supports composable and reusable components.
  • Template language takes cues from VueJS and AlpineJS, incorporating for/if directives within HTML tags.
  • No JavaScript required.
  • No code generation required, though a Go code generator can be developed for improved performance.
  • Implements file-based routing akin to NuxtJS.
  • Single-file components (combine HTML, CSS, and JS in a single file).
  • Not tied to Go ecosystem, allowing components to be reused in non-Go projects.
  • Plays nicely with HTMX and AlpineJS.
  • Automatic browser refresh during development.
  • Small API surface (both on the template language and the Go API) for quick starts.
  • Ability to embed assets into a single binary.

Why not html/template, jet, pongo2, or others?

Templates are harder to compose and reuse across projects compared to more modular components approach.

Why not templ or gomponents?

These options lock you into the Go ecosystem, limiting component reuse in non-Go projects.

Installation

go get -u github.com/dpotapov/go-pages

Example Usage

  1. Create a directory for your pages and components. For example, ./pages.

  2. Create a file in the ./pages directory. For example, ./pages/index.chtml with the following content:

    <h1>Hello World</h1>
    
  3. Create a Go program to serve the pages.

    package main
    
    import (
        "net/http"
        "os"
    
        "github.com/dpotapov/go-pages"
    )
    
    func main() {
        ph := &pages.Handler{
            FileSystem: os.DirFS("./pages"),
        }
    
        http.ListenAndServe(":8080", ph)
    }
    
  4. Run the program and navigate to http://localhost:8080. You should see the text "Hello World".

  5. Create another file in the ./pages directory. For example, ./pages/about.chtml with the following content:

    <h1>About page</h1>
    
  6. Navigate to http://localhost:8080/about. You should see the text "About page". No need to restart the server.

Check out the example directory for a more complete example.

CHTML Tags and Attributes

Components are defined in .chtml files in HTML5-like syntax.

On-top of a standard HTML, go-pages adds the following elements and attributes prefixed with c: namespace:

  • <c:NAME>...</c:NAME> imports a component by name. The body of the element is passed to the component as an argument and can be interpolated with ${_} syntax. Any attributes on the element are passed to the component as arguments as well. Typically, the component is a .chtml file, but it can also be a virtual component defined in Go code.

  • <c:attr name="ATTR_NAME">...</c:attr> - is a builtin component that adds an attribute named ATTR_NAME to the parent element.

  • c:if, c:else-if, c:else attribute for conditional rendering.

  • c:for attribute for iterating over a slice or a map.

All c: elements and attributes are removed from the final HTML output.

Kebab-case conversion

The go-pages library does not enforce a style for naming components and arguments, you may choose between CamelCase/camelCase or kebab-style, single word or multiple words. Just don't use underscores as they feel wrong in HTML and URL paths.

You may want to use kebab-case for components, that represent pages (and become part of the URL that visible to the user). When referencing a component or an argument in an expression, all dashes will be replaced with underscores.

Example:

<!--
  kebab-style - preferred for URLs and native to HTML.
  The go-pages engine will automatically replace dashes to underscore_case to make easier to use
  in expressions. E.g. some-arg-1 becomes ${some_arg_1}. 
 -->

<c:my-component some-arg-1="...">
   ...
</c:my-component>

<!--
  CamelCase for component names and camelCase for attributes.
  This style is easier to use in expressions. E.g. someArg1 can be referenced as ${someArg1}.
 -->
<c:MyComponent someArg1="...">
   ...
</c:MyComponent>

Expressions

Currently, go-pages uses the https://github.com/expr-lang/expr library for evaluating expressions. Refer https://expr-lang.org/ for the syntax.

String Interpolation

String interpolation is supported using the ${ ... } syntax. For example:

<a href="${link}">Hello, ${name}!</a>

All string attributes and text nodes are interpolated.

File-based Routing

go-pages handler implements a file-based routing system. The path from the request URL is used to find the corresponding .chtml file.

For example, if the request URL is /about, the handler will look for the about.chtml file in the directory. The handler will also look for an index.chtml file if the request URL ends with /.

If the request URL points to a static file (e.g. /css/style.css), the handler will serve the file if it exists in the file system.

Dynamic routes

Directories or component files can be prefixed with an underscore to indicate a dynamic route. Double underscore __ in the component filename is used to indicate a catch-all route.

Examples:

/_user
    /index.chtml    -> matches URL: /joe/
    /profile.chtml  -> matches URL: /joe/profile
/posts
    /index.chtml    -> matches URL: /posts/
    /_slug.chtml    -> matches URL: /posts/hello-world
/__path.chtml       -> matches URL: /anything/else

The router will pass the dynamic part of the URL as an argument to the component. For example, the /posts/_slug.chtml component will receive the slug argument with the value hello-world.

Catch-all component in the root directory of the file system can be used to implement a 404 page.

Each top-level page component receives a pages.RequestArg object as an request argument. Here is an example of the data it contains:

# HTTP method, string: GET, POST, etc.
method: "GET"

# Full URL
url: "http://localhost:8080/posts/hello-world?foo=bar&foo=baz"

# URL scheme
scheme: "http"

# Hostname part of the URL
host: "localhost"

# Port part of the URL
port: "8080"

# Path part of the URL
path: "/posts/hello-world"

# URL Query parameters, represented as a map of string slices.
query:
  foo: ["bar", "baz"]

# Remote address of the client, string: "IPv4:PORT" or "IPv6:PORT"
remote_addr: "127.0.0.1:12345"

# HTTP headers, represented as a map of string slices.
headers:
  Content-Type: ["application/json"]
  Cookie: ["session_id=1234567890", "user_id=123"]

# Body is available only when the content type is either application/json or
# application/x-www-form-urlencoded and the body size is less than xxx MB.
# TODO: define the size limit.
body:
  foo: "bar"
  bar: "baz"

# raw_body is only available when the content type is not application/json or
# application/x-www-form-urlencoded, or body size exceeds xxx MB limit.
# It is meant to be passed to custom components with Go renderers.
raw_body: nil

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewErrorHandlerComponent

func NewErrorHandlerComponent(name string, imp chtml.Importer, fallback chtml.Component) *errorHandlerComponent

Types

type CookieComponent

type CookieComponent struct{}

func (CookieComponent) Render

func (cc CookieComponent) Render(s chtml.Scope) (any, error)

type Handler

type Handler struct {
	// FileSystem to serve HTML components and other web assets from.
	FileSystem fs.FS

	// ComponentSearchPath is a list of directories in the FileSystem to search for CHTML components.
	// The list may contain absolute or relative paths. Relative paths are resolved
	// relative to the rendered component's path.
	//
	// If not set, the following default paths are used:
	// 1. "." (the directory of the rendered component)
	// 2. ".lib" (a directory named ".lib" in the directory of the rendered component)
	// 3. "/" (the root directory of the FileSystem)
	// 4. "/.lib" (a directory named ".lib" in the root directory of the FileSystem)
	ComponentSearchPath []string

	// CustomImporter is called to import user-defined components before looking in the FileSystem.
	// If CustomImporter returns chtml.ErrComponentNotFound, the default import process is used.
	CustomImporter chtml.Importer

	// BuiltinComponents is a map of built-in components that can be used in CHTML files.
	BuiltinComponents map[string]chtml.Component

	// OnError is a callback that is called when an error occurs while serving a page.
	OnError func(*http.Request, error)

	// OnErrorComponent is a name of a component that is rendered when an error occurs while
	// rendering a page.
	// This component is not invoked on general request processing errors where the OnError
	// callback can be used.
	// If not set, a standard "Internal Server Error" will be sent back to the client.
	OnErrorComponent string

	// Logger configures logging for internal events.
	Logger *slog.Logger
	// contains filtered or unexported fields
}

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements the http.Handler interface.

type HttpCallArgs

type HttpCallArgs struct {
	Method            string
	URL               string
	Interval          time.Duration
	BasicAuthUsername string
	BasicAuthPassword string
	Cookies           []*http.Cookie
	Header            http.Header
	Body              io.Reader
}

type HttpCallComponent

type HttpCallComponent struct {
	// contains filtered or unexported fields
}

HttpCallComponent implements a CHTML component for making HTTP requests and storing returned data in the scope.

func NewHttpCallComponent

func NewHttpCallComponent(router http.Handler) *HttpCallComponent

func (*HttpCallComponent) Dispose

func (c *HttpCallComponent) Dispose() error

func (*HttpCallComponent) Render

func (c *HttpCallComponent) Render(s chtml.Scope) (any, error)

type HttpCallResponse

type HttpCallResponse struct {
	Code  int    `expr:"code"`
	Body  string `expr:"body"`
	Json  any    `expr:"json"`
	Error string `expr:"error"`
}

type HttpResponseComponent

type HttpResponseComponent struct{}

func (HttpResponseComponent) Render

func (hc HttpResponseComponent) Render(s chtml.Scope) (any, error)

type RequestArg

type RequestArg struct {
	Method     string              `expr:"method"`
	URL        string              `expr:"url"`
	Host       string              `expr:"host"`
	Port       string              `expr:"port"`
	Scheme     string              `expr:"scheme"`
	Path       string              `expr:"path"`
	Query      map[string][]string `expr:"query"`
	RemoteAddr string              `expr:"remote_addr"`

	Headers map[string][]string `expr:"headers"`
	Cookies []*http.Cookie      `expr:"cookies"`

	// Body is available only when the content type is either application/json or
	// application/x-www-form-urlencoded.
	Body map[string]any `expr:"body"`

	// RawBody is the Body field of the http.Request. If the content type is parseable as JSON or
	// form data, the RawBody will be closed.
	RawBody io.ReadCloser `expr:"raw_body"`
}

RequestArg is a simplified model for http.Request suitable for expressions in templates.

func NewRequestArg

func NewRequestArg(r *http.Request) *RequestArg

type RequestComponent

type RequestComponent struct{}

func (RequestComponent) Render

func (rc RequestComponent) Render(s chtml.Scope) (any, error)

type RouteComponent

type RouteComponent struct{}

func (RouteComponent) Render

func (rc RouteComponent) Render(s chtml.Scope) (any, error)

Directories

Path Synopsis
Package chtml implements an HTML parser to be used by the `chtml` package.
Package chtml implements an HTML parser to be used by the `chtml` package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL