kelindar / folio

JSON document-based CMS, written with Go, SQLite & HTMX
MIT License
9 stars 1 forks source link
cms htmx sqlite

kelindar/folio
Go Version PkgGoDev Go Report Card License

Folio: Effortless Internal Tool Development

Folio is a tool I built to save time on the boring stuff. After years of building internal tools and getting stuck on UI work, I created Folio to automatically generate UIs from Go structs, letting me skip the front-end hassle and focus on the fun parts of development.

In this example, we define a Person struct with various fields and tags for validation and rendering. Folio automatically generates a user interface for the Person model, allowing users to create, read, update, and delete records.

type Person struct {
    folio.Meta `kind:"person" json:",inline"`
    Name       string    `json:"name" form:"rw" is:"required"`
    Age        int       `json:"age" form:"rw" is:"range(0|130)"`
    Gender     string    `json:"gender" form:"rw" is:"required,in(male|female|prefer_not_to)"`
    Country    string    `json:"country" form:"rw"`
    Address    string    `json:"address" form:"rw"`
    Phone      string    `json:"phone" form:"rw"`
    Boss       folio.URN `json:"boss" form:"rw" kind:"person"`
    IsEmployed bool      `json:"isEmployed" form:"rw" desc:"Is the person employed?"`
    JobTitle   string    `json:"jobTitle" form:"rw"`
    Workplace  folio.URN `json:"workplace" form:"rw" kind:"company" query:"namespace=*;match=Inc"`
}

The generated UI includes form fields for each struct field, as well as buttons for creating, updating, and deleting records. The UI also supports pagination, sorting, and filtering.

demo

Introduction

I’ve built a lot of internal tools over the years — everything from experimentation platforms to machine learning management tools. And while those tools were powerful, the process often felt like a never-ending cycle of reinventing the wheel, except this wheel was for a car that I didn’t really want to drive.

The problem? The minor stuff always took way more time and energy than it should. Need a UI for CRUD operations? That’ll be hours of React, CSS, and front-end misery. I just wanted to get things done, not spend my weekends pretending to enjoy writing JavaScript.

That’s where this project comes in. I built this for my personal projects where I have no team, no budget, and let’s be honest — no patience for building full-blown React apps. Folio generates the UI for me straight from my Go structs (view models), so I can focus on the fun parts (or at least the parts that don’t make me want to quit tech and become a beekeeper).

In short: Folio takes care of the boring stuff, so you can keep your focus on the good stuff—like actually building cool things instead of wrangling with endless form fields and dropdowns.

Keep in mind that this project is still in its early stages, so there’s a lot of room for improvement. I'm also not going to pretend that this is the best solution for every project, and there's still a ton of features that I want to add, so use it at your own risk.

🚀 Features

🛠 Getting Started

  1. Navigate to the company example directory:

    cd examples/company &&  go run .
  2. Open your browser and navigate to http://localhost:7000.

📚 Usage

Defining Models

Define your models by embedding folio.Meta and specifying field tags for validation and form rendering.

type Person struct {
    folio.Meta `kind:"person" json:",inline"`
    Name       string    `json:"name" form:"rw" is:"required"`
    Age        int       `json:"age" form:"rw" is:"range(0|130)"`
    Gender     string    `json:"gender" form:"rw" is:"required,in(male|female|prefer_not_to)"`
    Country    string    `json:"country" form:"rw"`
    Address    string    `json:"address" form:"rw"`
    Phone      string    `json:"phone" form:"rw"`
    Boss       folio.URN `json:"boss" form:"rw" kind:"person"`
    IsEmployed bool      `json:"isEmployed" form:"rw" desc:"Is the person employed?"`
    JobTitle   string    `json:"jobTitle" form:"rw"`
    Workplace  folio.URN `json:"workplace" form:"rw" kind:"company" query:"namespace=*;match=Inc"`
}

Registering Models

Register your models with the registry and provide options like icons, titles, and sorting.

reg := folio.NewRegistry()
folio.Register[*Person](reg, folio.Options{
    Icon:   "user-round",
    Title:  "Person",
    Plural: "People",
    Sort:   "1",
})

Starting the Server

Use the render.ListenAndServe function to start the server.

db, err := sqlite.Open("file:data.db?_journal_mode=WAL", reg)
if err != nil {
    panic(err)
}

if err := render.ListenAndServe(7000, reg, db); err != nil {
    slog.Error("Failed to start server!", "details", err.Error())
    os.Exit(1)
}

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

License

This project is licensed under the MIT License. See the LICENSE file for details.

Acknowledgements

This project leverages several open-source libraries and tools. We would like to acknowledge and thank the following projects: