feat: modernise template with core/go-api provider pattern

Replace Cobra CLI + raw net/http with core/go-api Engine.
DemoProvider implements RouteGroup for plug-and-play registration.
Lit element updated to fetch from Go API.
Add .core/build.yaml and CLAUDE.md.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-14 09:49:41 +00:00
parent a5de52ac6e
commit 5143c211d3
13 changed files with 295 additions and 123 deletions

7
.core/build.yaml Normal file
View file

@ -0,0 +1,7 @@
project: element-template
binary: core-element-template
targets:
- os: darwin
arch: arm64
- os: linux
arch: amd64

43
CLAUDE.md Normal file
View file

@ -0,0 +1,43 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with this repository.
## Project Overview
Starter template for building custom HTML elements backed by a Go API using the Core ecosystem. Clone this to create a new service provider that can plug into core/ide.
## Build & Development
```bash
# Build UI (Lit custom element)
cd ui && npm install && npm run build
# Run development server
go run . --port 8080
# Build binary
core build
# Quality assurance
core go qa
```
## Architecture
- **`main.go`** — Creates `api.Engine`, registers providers, serves embedded UI
- **`provider.go`** — Example `DemoProvider` implementing `api.RouteGroup`
- **`static.go`** — Static file serving helper
- **`ui/`** — Lit custom element that talks to the Go API via fetch
## Creating Your Own Provider
1. Rename `DemoProvider` in `provider.go`
2. Update `Name()`, `BasePath()`, and routes in `RegisterRoutes()`
3. Update the Lit element in `ui/src/index.ts` to match your API
4. Update the custom element tag in `ui/index.html`
## Conventions
- UK English in all user-facing strings
- EUPL-1.2 licence
- Conventional commits

View file

@ -1,43 +1,52 @@
# Core Element Template
This repository is a template for developers to create custom HTML elements for the core web3 framework. It includes a Go backend, a Lit-based custom element, and a full release cycle configuration.
Starter template for building custom HTML elements backed by a Go API. Part of the [Core ecosystem](https://core.help).
## Getting Started
1. **Clone the repository:**
```bash
git clone https://github.com/your-username/core-element-template.git
```
2. **Install the dependencies:**
```bash
cd core-element-template
go mod tidy
cd ui
npm install
```
3. **Run the development server:**
```bash
go run ./cmd/demo-cli serve
```
This will start the Go backend and serve the Lit custom element.
## Building the Custom Element
To build the Lit custom element, run the following command:
## Quick Start
```bash
cd ui
npm run build
# Clone and rename
git clone https://forge.lthn.ai/core/element-template.git my-element
cd my-element
# Install UI dependencies and build
cd ui && npm install && npm run build && cd ..
# Run
go run . --port 8080
```
This will create a JavaScript file in the `dist` directory that you can use in any HTML page.
Open `http://localhost:8080` — you'll see the `<core-demo-element>` fetching data from the Go API.
## Contributing
## What's Included
Contributions are welcome! Please feel free to submit a Pull Request.
| Component | Technology | Purpose |
|-----------|-----------|---------|
| Go backend | core/go-api (Gin) | REST API with CORS, middleware |
| Custom element | Lit 3 | Self-contained web component |
| Build config | `.core/build.yaml` | Cross-platform binary builds |
## License
## Making It Yours
This project is licensed under the EUPL-1.2 License - see the [LICENSE](LICENSE) file for details.
1. Update `go.mod` module path
2. Rename `DemoProvider` in `provider.go` — implement your API
3. Rename `CoreDemoElement` in `ui/src/index.ts` — implement your UI
4. Update the element tag in `ui/index.html`
## Service Provider Pattern
The `DemoProvider` implements `api.RouteGroup`:
```go
func (p *DemoProvider) Name() string { return "demo" }
func (p *DemoProvider) BasePath() string { return "/api/v1/demo" }
func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) {
rg.GET("/hello", p.hello)
}
```
Register it with `engine.Register(&DemoProvider{})` and it gets middleware, CORS, and OpenAPI for free. The same provider can plug into core/ide's registry.
## Licence
EUPL-1.2

View file

@ -1,24 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "demo-cli",
Short: "A demo CLI for the core-element-template",
Long: `A longer description that spans multiple lines and likely contains examples and usage of using your application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello from the demo CLI!")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View file

@ -1,33 +0,0 @@
package cmd
import (
"fmt"
"log"
"net/http"
"github.com/spf13/cobra"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Starts the HTTP server",
Long: `Starts the HTTP server to serve the frontend and the API.`,
Run: func(cmd *cobra.Command, args []string) {
http.HandleFunc("/api/v1/demo", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, world!")
})
fs := http.FileServer(http.Dir("./ui/dist"))
http.Handle("/", fs)
log.Println("Listening on :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
},
}
func init() {
rootCmd.AddCommand(serveCmd)
}

View file

@ -1,9 +0,0 @@
package main
import (
"github.com/your-username/core-element-template/cmd/demo-cli/cmd"
)
func main() {
cmd.Execute()
}

10
go.mod
View file

@ -1,9 +1,9 @@
module github.com/your-username/core-element-template
module forge.lthn.ai/core/element-template
go 1.24.3
go 1.26.0
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
forge.lthn.ai/core/go-api v0.1.0
forge.lthn.ai/core/go-log v0.0.1
forge.lthn.ai/core/go-ws v0.1.3
)

View file

@ -1,3 +0,0 @@
2025/11/14 15:44:19 Listening on :8080...
2025/11/14 15:44:19 listen tcp :8080: bind: address already in use
exit status 1

72
main.go Normal file
View file

@ -0,0 +1,72 @@
// SPDX-License-Identifier: EUPL-1.2
// core-element-template demonstrates the service provider pattern.
// It serves a Lit custom element backed by a Go API using core/go-api.
//
// Usage:
//
// go run . [--port 8080]
package main
import (
"context"
"embed"
"flag"
"io/fs"
"os"
"os/signal"
"syscall"
api "forge.lthn.ai/core/go-api"
"forge.lthn.ai/core/go-log"
"github.com/gin-gonic/gin"
)
//go:embed all:ui/dist
var uiAssets embed.FS
func main() {
port := flag.String("port", "8080", "HTTP server port")
flag.Parse()
logger := log.New("element-template")
// Create API engine with middleware
engine, err := api.New(
api.WithCORS(),
)
if err != nil {
logger.Error("failed to create API engine", "error", err)
os.Exit(1)
}
// Register the demo provider
engine.Register(&DemoProvider{})
// Serve the Lit custom element UI as static files
staticFS, err := fs.Sub(uiAssets, "ui/dist")
if err != nil {
logger.Error("failed to load UI assets", "error", err)
os.Exit(1)
}
engine.Router().NoRoute(gin.WrapH(
noCache(staticFS),
))
// Start server
ctx, cancel := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer cancel()
addr := ":" + *port
logger.Info("starting server", "addr", addr)
go func() {
if err := engine.Serve(ctx, addr); err != nil {
logger.Error("server error", "error", err)
}
}()
<-ctx.Done()
logger.Info("shutting down")
}

42
provider.go Normal file
View file

@ -0,0 +1,42 @@
// SPDX-License-Identifier: EUPL-1.2
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// DemoProvider is an example service provider that exposes a REST API.
// Replace this with your own provider implementation.
type DemoProvider struct{}
// Name implements api.RouteGroup.
func (p *DemoProvider) Name() string { return "demo" }
// BasePath implements api.RouteGroup.
func (p *DemoProvider) BasePath() string { return "/api/v1/demo" }
// RegisterRoutes implements api.RouteGroup.
func (p *DemoProvider) RegisterRoutes(rg *gin.RouterGroup) {
rg.GET("/hello", p.hello)
rg.GET("/status", p.status)
}
func (p *DemoProvider) hello(c *gin.Context) {
name := c.DefaultQuery("name", "World")
c.JSON(http.StatusOK, gin.H{
"message": "Hello, " + name + "!",
})
}
func (p *DemoProvider) status(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "running",
"uptime": time.Since(startTime).String(),
})
}
var startTime = time.Now()

17
static.go Normal file
View file

@ -0,0 +1,17 @@
// SPDX-License-Identifier: EUPL-1.2
package main
import (
"io/fs"
"net/http"
)
// noCache wraps a filesystem with cache-busting headers for development.
func noCache(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
fileServer.ServeHTTP(w, r)
})
}

View file

@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
<title>Core Element Demo</title>
<script type="module" src="/index.js"></script>
</head>
<body>
<hello-world-element name="Your Name"></hello-world-element>
<core-demo-element></core-demo-element>
</body>
</html>

View file

@ -1,30 +1,81 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
/**
* A simple "Hello World" custom element.
* A demo custom element backed by a Go API.
* Replace this with your own element implementation.
*
* @element hello-world-element
* @element core-demo-element
*/
@customElement('hello-world-element')
export class HelloWorldElement extends LitElement {
/**
* The name to say hello to.
* @attr
*/
@property({ type: String })
name = 'World';
@customElement('core-demo-element')
export class CoreDemoElement extends LitElement {
/** The API base URL. Defaults to current origin. */
@property({ type: String, attribute: 'api-url' })
apiUrl = '';
@state()
private message = 'Loading...';
@state()
private uptime = '';
static styles = css`
:host {
display: block;
border: solid 1px gray;
padding: 16px;
max-width: 800px;
font-family: system-ui, -apple-system, sans-serif;
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
max-width: 480px;
}
h2 {
margin: 0 0 1rem;
font-size: 1.25rem;
color: #1a1b26;
}
.status {
font-size: 0.875rem;
color: #64748b;
}
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
background: #dcfce7;
color: #166534;
}
`;
connectedCallback() {
super.connectedCallback();
this.fetchData();
}
private async fetchData() {
const base = this.apiUrl || window.location.origin;
try {
const [helloRes, statusRes] = await Promise.all([
fetch(`${base}/api/v1/demo/hello`),
fetch(`${base}/api/v1/demo/status`),
]);
const hello = await helloRes.json();
const status = await statusRes.json();
this.message = hello.message;
this.uptime = status.uptime;
} catch {
this.message = 'Failed to connect to API';
}
}
render() {
return html`<h1>Hello, ${this.name}!</h1>`;
return html`
<h2>${this.message}</h2>
<div class="status">
<span class="badge">running</span>
Uptime: ${this.uptime}
</div>
`;
}
}