Files
2026-04-05 12:02:53 +02:00

9.8 KiB

apply
apply
always

Development Guidelines

Styling

Every page has styling in a separate CSS file.

  • Page-specific styles should be placed in separate CSS files in the static/ directory
  • Example: The media page has its styles in static/media.css
  • General/shared styles are in static/styles.css

Single Page Implementation

Path constants

  • Define path constants for the root paths of the page only (not for sub-routes).
const MediaPath = "/media"
const MediaPathDe = "/medien"

Page struct and initializer

  • Define a Page struct that contains all routes and actions as fields.
  • Initialize routes in the NewPage constructor function.
type Page struct {
    // Routes
    ViewRoute   HtmxRoute
    StartRoute  HtmxRoute
    StopRoute   HtmxRoute

}

func NewPage(server *Server) *Page {
    p := &Page{}

    // Initialize routes
    p.ViewRoute = NewHtmxGetRoute(p.View, LanguagePaths{En: MediaPath, De: MediaPathDe}).SetActivePath(MediaPath)
    p.StartRoute = NewHtmxPostRoute(p.Start, LanguagePaths{En: MediaPath + "/start", De: MediaPathDe + "/start"})
    p.StopRoute = NewHtmxPostRoute(p.Stop, LanguagePaths{En: MediaPath + "/stop", De: MediaPathDe + "/stop"})

    // Register routes
    p.ViewRoute.Add(server)
    p.StartRoute.Add(server)
    p.StopRoute.Add(server)

    return p
}

Multilingual content (I18nText)

  • Declare user-facing text with I18nText; avoid hardcoded strings.
  • Create a separate Texts struct type and initialize it in a createTexts() function.
var texts *Texts

type Texts struct {
    PageTitle       I18nText
    PageDescription I18nText
    HeroTitle       I18nText
    StartProcessing I18nText
}

func createTexts() {
    texts = &Texts{
        PageTitle:       NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}),
        PageDescription: NewI18nText(map[Language]string{En: "Process and organize your media files", De: "Verarbeiten und organisieren Sie Ihre Mediendateien"}),
        HeroTitle:       NewI18nText(map[Language]string{En: "Media Processing", De: "Medienverarbeitung"}),
        StartProcessing: NewI18nText(map[Language]string{En: "Start Processing", De: "Verarbeitung starten"}),
    }
}

func (p *Page) getMetaData() PageWebsiteMetaData {
    return PageWebsiteMetaData{
        Title:       texts.PageTitle,
        Lang:        En,
        Description: texts.PageDescription,
        StyleSrcs:   []string{"/static/media.css"},
    }
}

func (p *Page) View(c HtmxContext) {
    lang := c.GetLanguage()
    content := Group{
        Div(Class("hero"),
            H1(Text(texts.HeroTitle.FromLang(lang))),
        ),
    }
    p.createPage(c, p.getMetaData(), content)
}

Language-aware URLs and hx attributes

  • Derive URLs from routes with the active language for all hx attributes.
lang := c.GetLanguage()

Button(
    Text(texts.StartProcessing.FromLang(lang)),
    p.StartRoute.GetHtmx(lang),
    hx.Target("body"),
)

// Pagination example with hx.PushURL to keep browser URL in sync
Button(
    Text(texts.Next.FromLang(lang)),
    hx.Get(p.RefreshRoute.ToUrlFromContext(c, lang)),
    hx.PushURL(p.ViewRoute.ToUrlFromContext(c, lang)),
    hx.Target("#queue-contents"),
    hx.Swap("innerHTML"),
)

Page assembly and styles

  • Build pages with createPage and supply metadata plus styles.
  • Create a getMetaData() function to centralize page metadata configuration.
  • Keep shared CSS in static/styles.css and page-specific CSS in static/<page>.css; include via StyleSrcs.
func (p *Page) getMetaData() PageWebsiteMetaData {
    return PageWebsiteMetaData{
        Title:       texts.PageTitle,
        Lang:        En,
        Description: texts.PageDescription,
        StyleSrcs:   []string{"/static/media.css"},
    }
}

func (p *Page) View(c HtmxContext) {
    lang := c.GetLanguage()
    content := Group{
        Div(Class("hero"), H1(Text(texts.HeroTitle.FromLang(lang)))),
        // ... more components ...
    }

    p.createPage(c, p.getMetaData(), content)
}

HTMX partials and polling

  • Use HTMX forms and partial refreshes; combine hx.Target, hx.Swap, and periodic polling (hx.Trigger("every 5s")) with language-aware URLs.
// Partial that refreshes every 5s when running
Group{
    hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang)),
    hx.Trigger("every 5s"),
    hx.Target("#queue-status"),
    hx.Swap("innerHTML"),
}

Form(
    p.SaveSettingsRoute.GetHtmx(lang),
    hx.Target("body"),
    // form fields...
)

// Use GetHtmx() for form actions to ensure proper HTMX attributes
Form(
    p.localLoginRoute.GetHtmx(language),
    hx.Target("body"),
    // form fields...
)

HTMX Routes

ALWAYS prefer using p.Route.GetHtmx() over directly calling hx.Get(p.Route.ToUrlFromContext()).

Example:

  • Preferred: p.RefreshStatusRoute.GetHtmx(c, lang)
  • Avoid: hx.Get(p.RefreshStatusRoute.ToUrlFromContext(c, lang))

Persisting Data with Ent Schemas

Schema Definition

  • Define schemas in ent/schema/<entity>.go
  • Each schema is a struct that embeds ent.Schema
  • Use the TimeMixin to automatically add created_at and updated_at fields
package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/schema/field"
	"entgo.io/ent/schema/edge"
)

type Media struct {
	ent.Schema
}

// Apply TimeMixin for automatic timestamp fields
func (Media) Mixin() []ent.Mixin {
	return []ent.Mixin{
		TimeMixin{},
	}
}

// Define fields
func (Media) Fields() []ent.Field {
	return []ent.Field{
		field.String("filename"),
		field.String("hash"),
		field.String("description").Default(""),
		field.Time("created_date").Optional().Nillable(),
		field.Int("year").Optional().Nillable(),
		field.Float("gps_latitude").Optional().Nillable(),
	}
}

// Define relationships
func (Media) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("tags", Tag.Type),  // Many-to-Many
	}
}

Field Types and Options

Common field types:

  • field.String("name") - string field
  • field.Int("count") - integer field
  • field.Float("latitude") - float64 field
  • field.Bool("active") - boolean field
  • field.Time("date") - time.Time field

Field modifiers:

  • .Default(value) - set default value
  • .Optional() - field can be omitted on creation
  • .Nillable() - field can be nil (must be used with Optional)
  • .Immutable() - field cannot be updated after creation
  • .Unique() - field value must be unique

Generating Code

After modifying schemas, regenerate the ent code:

go run generate_schema.go

Usage in Handlers

Create:

media, err := server.Client.Media.Create().
	SetFilename("photo.jpg").
	SetHash("abc123").
	SetDescription("Vacation photo").
	Save(c.Request.Context())

Query:

// Get by ID
media, err := server.Client.Media.Get(c.Request.Context(), id)

// Query with filters
medias, err := server.Client.Media.Query().
	Where(media.YearEQ(2024)).
	All(c.Request.Context())

// Query with edges
medias, err := server.Client.Media.Query().
	WithTags().  // Load tags relationship
	All(c.Request.Context())

Update:

err := server.Client.Media.UpdateOneID(id).
	SetDescription("Updated description").
	Save(c.Request.Context())

Delete:

err := server.Client.Media.DeleteOneID(id).
	Exec(c.Request.Context())

Relationships (Edges)

One-to-Many:

edge.To("posts", Post.Type)  // User has many posts
edge.From("owner", User.Type).Ref("posts").Unique()  // Post belongs to one user

Many-to-Many:

edge.To("tags", Tag.Type)  // Media has many tags
edge.From("media", Media.Type).Ref("tags")  // Tag has many media

Workflows

Defining Steps

  • Define a workflow step using NewStep and StepParams.
  • Each step requires a Name, Identifier, and a Handler function.
  • The Handler function processes the job and returns a result and the next step (if any).
lastStep := NewStep(&StepParams{
    Name:        "Last step",
    Identifier:  "last_step",
    Description: "Last step",
    Client:      client,
    MaxRetries:  3,
    Handler: func(ctx context.Context, currentStep *Step, job queue.GeneralQueueJob) (queue.GeneralQueueHandlerResult, *Step, error) {
        return queue.GeneralQueueHandlerResult{
            ResultPayload: map[string]interface{}{
                "lastStep": "hello",
            },
        }, nil, nil
    },
})

firstStep := NewStep(&StepParams{
    Name:        "Initial step",
    Identifier:  "initial_step",
    Description: "Initial step",
    Client:      client,
    MaxRetries:  3,
    Handler: func(ctx context.Context, currentStep *Step, job queue.GeneralQueueJob) (queue.GeneralQueueHandlerResult, *Step, error) {
        return queue.GeneralQueueHandlerResult{
            ResultPayload: map[string]interface{}{
                "firstStep": "hello",
            },
        }, lastStep, nil // Return the next step to execute
    },
})

Creating Workflows

  • Define a workflow using NewWorkflow, providing a name, identifier, starting step, and all steps in the workflow.
w := NewWorkflow("Example", "example", firstStep, []*Step{firstStep, lastStep})

Executing Workflows

  • To start a workflow, call the Execute method with an initial payload.
  • A unique workflowId is automatically generated and passed through each step.
err := workflow.Execute(ctx, map[string]interface{}{
    "input": "start data",
})

Scheduled Workflows

  • Use NewCronTrigger to run a workflow at regular intervals.
NewCronTrigger(ctx, workflow, 1 * time.Hour)

Important Instructions

Do what has been asked; nothing more, nothing less.

NEVER create files unless they're absolutely necessary for achieving your goal.

ALWAYS prefer editing an existing file to creating a new one.

NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.