9.8 KiB
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
Pagestruct that contains all routes and actions as fields. - Initialize routes in the
NewPageconstructor 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
Textsstruct type and initialize it in acreateTexts()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
createPageand supply metadata plus styles. - Create a
getMetaData()function to centralize page metadata configuration. - Keep shared CSS in
static/styles.cssand page-specific CSS instatic/<page>.css; include viaStyleSrcs.
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
TimeMixinto automatically addcreated_atandupdated_atfields
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 fieldfield.Int("count")- integer fieldfield.Float("latitude")- float64 fieldfield.Bool("active")- boolean fieldfield.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
NewStepandStepParams. - Each step requires a
Name,Identifier, and aHandlerfunction. - The
Handlerfunction 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
Executemethod with an initial payload. - A unique
workflowIdis automatically generated and passed through each step.
err := workflow.Execute(ctx, map[string]interface{}{
"input": "start data",
})
Scheduled Workflows
- Use
NewCronTriggerto 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.