Skip to content
yinebebtPublic

About

Short link generation, click tracking, and OG preview pages for Go. Pluggable processors, Redis or in-memory storage.

Topics

Resources

Stars

13 stars

Watchers

1 watching

Forks

Latest commit

 

History

27 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

deeplink

Short link generation, click analytics, link expiry, and OG preview pages for Go, with MCP tools so AI clients can manage links too. Pluggable processors; in-memory, Redis, or SQLite storage; one dependency in the core.

CI Go Reference License: MIT

Install

go get github.com/yinebebt/deeplink

Usage

service, err := deeplink.New(deeplink.Config{
    BaseURL:     "https://link.example.com",
    Store:       deeplink.NewMemoryStore(),
    TemplateDir: "templates/default",
})
if err != nil {
    log.Fatal(err)
}
service.Register(deeplink.RedirectProcessor{})

// Mount alongside your own routes.
mux := http.NewServeMux()
mux.Handle("/", service.Handler())
mux.HandleFunc("GET /hello", yourHandler)

log.Fatal(http.ListenAndServe(":8090", mux))

Create a short link:

curl -X POST http://localhost:8090/shorten \
  -H 'Content-Type: application/json' \
  -d '{"type":"redirect","url":"https://example.com/docs","title":"Docs"}'

Open the returned short_url in a browser.

Custom processors

Implement Processor:

type Processor interface {
    Type() string
    Process(ctx context.Context, link *Link) error
}

For custom template data, also implement Previewer:

type Previewer interface {
    Preview(link *Link) any
}

See example/custom for a working custom processor with tests.

Standalone server

SQLite by default (zero infra). Redis is optional for multi-instance setups:

go run ./cmd/deeplink

# Redis:
docker compose up -d
DEEPLINK_STORE=redis go run ./cmd/deeplink

Stores

Any Store implementation works; the destination determines persistence. All built-in stores carry the full feature set — link expiry and click analytics included:

Store Import Persistent Analytics storage
deeplink.NewMemoryStore() core no aggregate maps
redisstore.New(client) deeplink/redisstore yes aggregate hashes (HINCRBY)
sqlitestore.New(dsn) deeplink/sqlitestore yes per-visit event rows
import "github.com/yinebebt/deeplink/sqlitestore"

store, err := sqlitestore.New("deeplink.db")

The Redis and SQLite stores live in subpackages, so their drivers (go-redis, modernc.org/sqlite) are only pulled in when you import them — the core stays at a single dependency (go-nanoid). Need another backend? Implement the Store interface, including the two analytics methods (RecordEvents and Stats).

Link expiry

Set expires_at (RFC 3339, must be in the future) to make a link self-destruct. After it passes, the link resolves as 404 and stores purge it (Redis via a native TTL, others lazily on read):

curl -X POST http://localhost:8090/shorten \
  -H 'Content-Type: application/json' \
  -d '{"type":"redirect","url":"https://example.com","expires_at":"2026-12-31T23:59:59Z"}'

Omit expires_at for a link that never expires.

Click analytics

Every resolved visit is captured off the redirect path and flushed in the background — bumping the click counter and recording the event for breakdowns, which every store supports. Query them at GET /stats/{shortID}:

{
  "short_id": "aBcD…",
  "clicks": 1280,
  "by_platform": {"android": 700, "ios": 500, "web": 80},
  "by_referrer": {"t.co": 410, "facebook.com": 260},
  "by_day":      {"2026-06-13": 640, "2026-06-14": 640}
}

Platform, device, browser, OS, and referrer are derived from the request.

MCP server

The mcpserver subpackage exposes the same operations as MCP tools, built on the official Go SDK.

Tool Does
create_link Create a short link with optional preview fields and expiry
get_link Destination, preview fields, and click count
list_links Links with click counts, optionally by type (default 50)
link_stats Click breakdowns by platform, referrer, and day
delete_link Soft-delete a link
list_types Link types create_link accepts

Mount it next to the HTTP handler:

import "github.com/yinebebt/deeplink/mcpserver"

mux.Handle("/mcp", mcpserver.Handler(service, nil))

It serves stateless streamable HTTP and does not authenticate. cmd/deeplink serves it at /mcp behind DEEPLINK_API_KEY. To connect Claude Code:

claude mcp add --transport http deeplink https://link.example.com/mcp \
  --header "Authorization: Bearer $DEEPLINK_API_KEY"

HTTP routes

Method Path Description
POST /shorten Create a short link
PATCH /{shortID} Update mutable fields on a link
DELETE /{shortID} Soft-delete a link (3h grace)
GET /{shortID} Preview page (or 302 redirect)
GET /links All links across types (dashboard data source)
GET /links/{type} List links by type
GET /links/{type}/{shortID} Link detail with click count
GET /stats/{shortID} Click analytics breakdowns
GET /health Health check

When any store URL is set (AndroidStoreURL, IOSStoreURL, WebFallbackURL), these are also registered:

Method Path Description
GET /preview/{shortID} Preview without auto-redirect
GET /redirect App store redirect by platform
GET /.well-known/ Static files from template dir

For iOS Universal Links and Android App Links, place your apple-app-site-association and assetlinks.json files in <TemplateDir>/.well-known/.

Configuration

cmd/deeplink reads its configuration from environment variables — see .env.example for the full list with defaults. Setting DEEPLINK_API_KEY enables auth on mutating endpoints, on GET /links and GET /stats/{id}, and on /mcp, sent as Authorization: Bearer <key> or X-API-Key. Preview and redirect GETs stay public. Rotate the key if it has ever been pasted into chat or committed.

Templates

The default templates live in templates/default/; copy them and point TemplateDir at the copy to customize. They render the preview metadata fields plus {{.ShortURL}}, {{.Lang}}, and the store-fallback URLs ({{.AndroidStoreURL}}, {{.IOSStoreURL}}, {{.WebFallbackURL}}).

The default link.html tries the destination first (so an installed app opens via its Universal/App Link) and, after a short timeout, falls back to the right app store by platform — with no store URLs it just forwards to the destination.

Link preview metadata

Preview pages emit Open Graph, Twitter Card, and fediverse tags. Empty fields are omitted, so scrapers never see content="".

Per-link fields (Link)

Field Effect
Title <title>, og:title, twitter:title
Description description, og:description, twitter:description
ImageURL og:image, twitter:image. PNG/JPG/WebP only (SVG fails most scrapers); 1200x630 recommended
ImageWidth / ImageHeight og:image:width / og:image:height
ImageAlt og:image:alt, twitter:image:alt
OGType og:type (defaults to website). Setting article also emits article:published_time and article:modified_time
Locale og:locale. Falls back to Config.Locale
UpdatedAt og:updated_time. Set automatically on create

Service-wide fields (Config)

Field Effect
SiteName og:site_name
Locale Default og:locale when Link.Locale is empty
TwitterSite twitter:site (e.g. @example)
FediverseCreator fediverse:creator (e.g. @user@instance.tld)

Pages also emit robots: noindex,follow so short links don't compete with the destination URL in search.

Dashboard / site UI

The demo UI is on the web branch.

Development

go test ./...          # run tests
go run ./cmd/deeplink  # standalone server (SQLite by default)

License

MIT

About

Short link generation, click tracking, and OG preview pages for Go. Pluggable processors, Redis or in-memory storage.

Topics

Resources

Stars

13 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages