Files
ersteller/starter/AGENTS.md
T
2026-01-07 20:38:24 +01:00

317 lines
7.9 KiB
Markdown

# 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).
```go
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.
```go
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.
```go
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.
```go
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`.
```go
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.
```go
// 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
```go
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:
```bash
go run generate_schema.go
```
### Usage in Handlers
**Create:**
```go
media, err := server.Client.Media.Create().
SetFilename("photo.jpg").
SetHash("abc123").
SetDescription("Vacation photo").
Save(c.Request.Context())
```
**Query:**
```go
// 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:**
```go
err := server.Client.Media.UpdateOneID(id).
SetDescription("Updated description").
Save(c.Request.Context())
```
**Delete:**
```go
err := server.Client.Media.DeleteOneID(id).
Exec(c.Request.Context())
```
### Relationships (Edges)
**One-to-Many:**
```go
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:**
```go
edge.To("tags", Tag.Type) // Media has many tags
edge.From("media", Media.Type).Ref("tags") // Tag has many media
```
## 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.