producthunt

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package producthunt is the library behind the ph command line: the HTTP client for both planes, the offline reference layer, and the typed records read from public Product Hunt surfaces.

Product Hunt has two planes. The web plane (www.producthunt.com) is the default, but every page is fronted by Cloudflare, so the one anonymous surface that survives is the Atom feed at /feed; a walled response becomes ErrBlocked before any parser runs. The api plane (api.producthunt.com/v2/api/graphql) is the opt-in upgrade, turned on by a PRODUCTHUNT_TOKEN or a PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET in the environment, and reads reliably from anywhere. The Client below fetches both, paces and retries politely, caches on disk, and turns a walled or rejected response into a typed error the exit-code mapping understands.

Index

Constants

View Source
const (
	// Host is the public website, the web plane, and the host the URI driver claims.
	Host = "www.producthunt.com"
	// BaseURL is the root of the web plane.
	BaseURL = "https://" + Host
	// FeedURL is the one anonymous web surface that survives the Cloudflare wall.
	FeedURL = BaseURL + "/feed"
	// APIURL is the GraphQL endpoint of the opt-in api plane.
	APIURL = "https://api.producthunt.com/v2/api/graphql"
	// OAuthURL mints a public-scope token from an application's client credentials.
	OAuthURL = "https://api.producthunt.com/v2/oauth/token"

	// DefaultPlane is the automatic, environment-driven plane choice.
	DefaultPlane = "auto"

	// DefaultCacheTTL is how long a cached response stays fresh by default. Product
	// Hunt runs on a daily cadence, so a few hours is plenty.
	DefaultCacheTTL = 6 * time.Hour
)
View Source
const DefaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " +
	"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36"

DefaultUserAgent identifies the client. It names a current browser, because the web plane is fronted by Cloudflare and an obviously scripted agent is turned away faster; it is still honest in that the tool does not forge a crawler identity it is reverse-DNS-checked against.

Variables

View Source
var (
	// ErrBlocked is the wall: a Cloudflare 403/503 or challenge interstitial on the
	// web plane, or a rejected/missing token (401, invalid_oauth_token) on the api
	// plane. It maps to need-auth (exit 4), with a remedy of reading the public Atom
	// feed or setting free Product Hunt API credentials.
	ErrBlocked = errors.New("blocked: Product Hunt's web pages are walled by Cloudflare here. The public Atom feed (ph feed) needs no account; for the full API set PRODUCTHUNT_TOKEN, or PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET")

	// ErrNeedKey is an api-only surface reached with no credentials. Maps to exit 4.
	ErrNeedKey = errors.New("this surface needs the Product Hunt API: set PRODUCTHUNT_TOKEN, or PRODUCTHUNT_CLIENT_ID and PRODUCTHUNT_CLIENT_SECRET (free credentials from https://www.producthunt.com/v2/oauth/applications)")

	// ErrNotFound is a missing entity (a null node for a single lookup). Maps to exit 6.
	ErrNotFound = errors.New("not found")

	// ErrRateLimited is a sustained 429 after retries, or the GraphQL complexity cap.
	// Maps to exit 5.
	ErrRateLimited = errors.New("rate limited by Product Hunt: slow down with --rate or try again later")

	// ErrNetwork is a transport failure that survives every retry: a connection
	// refused, a timeout, a DNS error, or a 5xx that never recovered. Maps to exit 8.
	ErrNetwork = errors.New("network error reaching Product Hunt")

	// ErrUsage is a bad argument the library catches (an unrecognized reference, a
	// bad order). Maps to exit 2.
	ErrUsage = errors.New("usage")
)

Sentinel errors the library returns; domain.go's mapErr turns each into the kit error kind that carries the right exit code (see the spec section 4.5).

Functions

func Defaults added in v0.2.0

func Defaults(c *kit.Config)

Defaults seeds the framework baseline with producthunt's own values, so an unset --rate or --timeout uses the producthunt default rather than the generic kit one.

func Identity added in v0.2.0

func Identity() kit.Identity

Identity is the fixed description of the Product Hunt CLI, shared by the domain and the standalone composition root so help and version read the same.

func URLFor added in v0.2.0

func URLFor(kind, id string) string

URLFor builds an addressable URL for a (kind, id), or "" if it cannot. A page URL needs the human slug, so a slug or username rebuilds the page directly; a bare numeric post id cannot rebuild the discussion URL offline (it needs the slug), so it returns the stable /r/p/<n> redirect, the one addressable URL a numeric post id can build. A record's own url carries the discussion page after a fetch.

Types

type Client

type Client struct {
	HTTP *http.Client
	// contains filtered or unexported fields
}

Client reads public Product Hunt data over HTTP on both planes.

func ClientFromConfig added in v0.2.0

func ClientFromConfig(cfg kit.Config) *Client

ClientFromConfig maps the framework config onto a producthunt.Config and returns a client. The credentials are read from the environment in DefaultConfig, never from a flag.

func NewClient

func NewClient(cfg Config) *Client

NewClient returns a Client configured from cfg, filling unset fields with their defaults.

func (*Client) CollectionAPI added in v0.2.0

func (c *Client) CollectionAPI(ctx context.Context, ref string) (*Collection, error)

CollectionAPI reads one collection with its posts.

func (*Client) CollectionsAPI added in v0.2.0

func (c *Client) CollectionsAPI(ctx context.Context, n int) ([]*Collection, error)

CollectionsAPI lists collections, paging up to n.

func (*Client) CommentsAPI added in v0.2.0

func (c *Client) CommentsAPI(ctx context.Context, ref string, n int) ([]*Comment, error)

CommentsAPI reads a post's comment thread, paging up to n. Each top-level comment is followed by its replies, with the parent edge set.

func (*Client) Feed added in v0.2.0

func (c *Client) Feed(ctx context.Context, limit int) ([]*Post, error)

Feed fetches the Atom feed and returns up to limit recent posts, newest first as the feed orders them.

func (*Client) FeedPost added in v0.2.0

func (c *Client) FeedPost(ctx context.Context, ref string) (*Post, error)

FeedPost returns the one feed post whose id or slug matches ref, or ErrNotFound when it is not in the recent feed. This is the keyless fallback for `ph post`.

func (*Client) PostAPI added in v0.2.0

func (c *Client) PostAPI(ctx context.Context, ref string) (*Post, error)

PostAPI reads one post by numeric id or slug.

func (*Client) PostsAPI added in v0.2.0

func (c *Client) PostsAPI(ctx context.Context, n int) ([]*Post, error)

PostsAPI reads the ranked, filtered post stream, paging up to n.

func (*Client) TopicAPI added in v0.2.0

func (c *Client) TopicAPI(ctx context.Context, ref string) (*Topic, error)

TopicAPI reads one topic with its top posts.

func (*Client) TopicsAPI added in v0.2.0

func (c *Client) TopicsAPI(ctx context.Context, n int) ([]*Topic, error)

TopicsAPI lists topics, paging up to n.

func (*Client) UserAPI added in v0.2.0

func (c *Client) UserAPI(ctx context.Context, ref string) (*User, error)

UserAPI reads one user with the posts they made.

type Collection added in v0.2.0

type Collection struct {
	ID          string      `json:"id" kit:"id"`
	Name        string      `json:"name,omitempty" table:",truncate"`
	Tagline     string      `json:"tagline,omitempty" table:",truncate"`
	Description string      `json:"description,omitempty" table:",truncate" kit:"body"`
	Slug        string      `json:"slug,omitempty"`
	Followers   int         `json:"followers,omitempty"`
	CoverImage  string      `json:"cover_image,omitempty" table:"-"`
	CreatedAt   string      `json:"created_at,omitempty" table:"-"`
	FeaturedAt  string      `json:"featured_at,omitempty" table:"-"`
	Curator     string      `json:"curator,omitempty" table:"-"` // the owner's username
	CuratorName string      `json:"curator_name,omitempty" table:"-"`
	Topics      []TopicLink `json:"topics,omitempty" table:"-"`
	URL         string      `json:"url"`
	CuratorRef  string      `json:"curator_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"`
	TopicRefs   []string    `json:"topic_refs,omitempty" table:"-" kit:"link,kind=producthunt/topic"`
	PostRefs    []string    `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"`
}

Collection is a curated list of posts, emitted by collections and collection.

type Comment added in v0.2.0

type Comment struct {
	ID         string `json:"id" kit:"id"`
	Post       string `json:"post,omitempty" table:"-" kit:"link,kind=producthunt/post"` // the post id
	Parent     string `json:"parent,omitempty" table:"-"`                                // parent comment id, for replies
	Body       string `json:"body,omitempty" table:",truncate" kit:"body"`
	Votes      int    `json:"votes,omitempty"`
	CreatedAt  string `json:"created_at,omitempty"`
	Author     string `json:"author,omitempty"` // username
	AuthorName string `json:"author_name,omitempty" table:"-"`
	URL        string `json:"url,omitempty" table:"-"`
	UserRef    string `json:"user_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"`
}

Comment is one comment on a post, emitted by comments. Post is the edge back to the commented post; an author is a username and a display name, never a fabricated member node.

type Config

type Config struct {
	UserAgent string
	Delay     time.Duration // minimum gap between requests
	Retries   int           // retries on 429/5xx
	Timeout   time.Duration // per-request timeout

	Plane        string // "web", "api", or "auto"
	Token        string // a ready bearer token, from PRODUCTHUNT_TOKEN only
	ClientID     string // an application id, from PRODUCTHUNT_CLIENT_ID only
	ClientSecret string // its secret, from PRODUCTHUNT_CLIENT_SECRET only

	// List shaping, shared by the read commands.
	Order    string // ranking, newest, votes, featured (per surface)
	Topic    string // a topic slug to filter posts
	Featured bool   // restrict to featured posts or collections
	After    string // postedAfter, an ISO date
	Before   string // postedBefore, an ISO date

	BaseURL  string // overridable for tests
	APIURL   string // overridable for tests
	OAuthURL string // overridable for tests

	CacheDir string
	NoCache  bool
	CacheTTL time.Duration
	Refresh  bool // refetch and rewrite the cache, ignoring any hit
}

Config is the resolved settings a Client reads.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the baseline settings and reads the credentials from the environment.

type Domain added in v0.2.0

type Domain struct{}

Domain is the producthunt driver. It carries no state; the per-run client is built by the factory Register hands kit.

func (Domain) Classify added in v0.2.0

func (Domain) Classify(input string) (uriType, id string, err error)

Classify turns any accepted input into the canonical (type, id), so `ant resolve` and `ant url` touch no network.

func (Domain) Info added in v0.2.0

func (Domain) Info() kit.DomainInfo

Info describes the scheme, the hostnames a pasted link is matched against, and the identity reused for the binary's help and version.

func (Domain) Locate added in v0.2.0

func (Domain) Locate(uriType, id string) (string, error)

Locate is the inverse: the addressable URL for a (type, id).

func (Domain) Register added in v0.2.0

func (Domain) Register(app *kit.App)

Register installs the client factory and every operation onto app. The read group is the data; the crawl group holds the keyless feed, the seed a host walks from to reach the rest of the graph; the ref group is offline and never touches the network.

type Maker added in v0.2.0

type Maker struct {
	ID       string `json:"id,omitempty"`
	Username string `json:"username,omitempty"`
	Name     string `json:"name,omitempty"`
}

Maker is the embedded reference to a person who made a post.

type Media added in v0.2.0

type Media struct {
	Type     string `json:"type,omitempty"` // image, video
	URL      string `json:"url,omitempty"`
	VideoURL string `json:"video_url,omitempty"`
}

Media is one image or video in a post's gallery or its thumbnail.

type Post added in v0.2.0

type Post struct {
	ID            string        `json:"id" kit:"id"` // the numeric post id, e.g. "1173164"
	Name          string        `json:"name,omitempty" table:",truncate"`
	Tagline       string        `json:"tagline,omitempty" table:",truncate"`
	Slug          string        `json:"slug,omitempty"`
	Description   string        `json:"description,omitempty" table:",truncate" kit:"body"`
	Votes         int           `json:"votes,omitempty"`
	Comments      int           `json:"comments,omitempty"`
	ReviewsCount  int           `json:"reviews_count,omitempty" table:"-"`
	ReviewsRating float64       `json:"reviews_rating,omitempty" table:"-"`
	Featured      bool          `json:"featured,omitempty" table:"-"`
	FeaturedAt    string        `json:"featured_at,omitempty" table:"-"`
	CreatedAt     string        `json:"created_at,omitempty"`
	Website       string        `json:"website,omitempty" table:"-"` // the outbound product URL
	Thumbnail     string        `json:"thumbnail,omitempty" table:"-"`
	Media         []Media       `json:"media,omitempty" table:"-"`
	ProductLinks  []ProductLink `json:"product_links,omitempty" table:"-"`
	Topics        []TopicLink   `json:"topics,omitempty" table:"-"`
	Hunter        string        `json:"hunter,omitempty" table:"-"` // the hunter's username
	HunterName    string        `json:"hunter_name,omitempty" table:"-"`
	Makers        []Maker       `json:"makers,omitempty" table:"-"`
	URL           string        `json:"url"`                                                                   // the discussion page
	CommentsRef   string        `json:"comments_ref,omitempty" table:"-" kit:"link,kind=producthunt/comments"` // = ID
	HunterRef     string        `json:"hunter_ref,omitempty" table:"-" kit:"link,kind=producthunt/user"`       // hunter username
	MakerRefs     []string      `json:"maker_refs,omitempty" table:"-" kit:"link,kind=producthunt/user"`       // maker usernames
	TopicRefs     []string      `json:"topic_refs,omitempty" table:"-" kit:"link,kind=producthunt/topic"`      // topic slugs
}

Post is the core record: a product launch. The id is the numeric post id, the same id the website and the Atom feed use, so a web read and an API read address the same record.

type ProductLink struct {
	Type string `json:"type,omitempty"` // website, app_store, play_store, github, ...
	URL  string `json:"url,omitempty"`
}

ProductLink is one outbound link a post carries (website, app store, repo).

type Ref added in v0.2.0

type Ref struct {
	Input string `json:"input"`
	Kind  string `json:"kind"`
	ID    string `json:"id"`
	URL   string `json:"url"`
}

Ref is the result of `ph ref id`: the canonical (kind, id) a reference resolves to, plus the URL, all without touching the network.

func Classify added in v0.2.0

func Classify(input string) Ref

Classify resolves a reference offline. It accepts a full Product Hunt URL, a path, an Atom id, a @username, or a bare id/slug.

type Topic added in v0.2.0

type Topic struct {
	ID          string   `json:"id" kit:"id"`
	Name        string   `json:"name,omitempty"`
	Slug        string   `json:"slug,omitempty"`
	Description string   `json:"description,omitempty" table:",truncate" kit:"body"`
	Followers   int      `json:"followers,omitempty"`
	PostsCount  int      `json:"posts_count,omitempty"`
	Image       string   `json:"image,omitempty" table:"-"`
	CreatedAt   string   `json:"created_at,omitempty" table:"-"`
	URL         string   `json:"url"`
	PostRefs    []string `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"` // its top posts
}

Topic is a subject tag a post can carry, emitted by topics and topic. PostRefs is its top posts, each a walkable post edge.

type TopicLink struct {
	ID   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
	Slug string `json:"slug,omitempty"`
}

TopicLink is the embedded reference to a topic inside a post or a collection.

type User added in v0.2.0

type User struct {
	ID        string   `json:"id" kit:"id"`
	Username  string   `json:"username,omitempty"`
	Name      string   `json:"name,omitempty"`
	Headline  string   `json:"headline,omitempty" table:",truncate"`
	Twitter   string   `json:"twitter,omitempty" table:"-"`
	Website   string   `json:"website,omitempty" table:"-"`
	Image     string   `json:"image,omitempty" table:"-"`
	Cover     string   `json:"cover,omitempty" table:"-"`
	CreatedAt string   `json:"created_at,omitempty" table:"-"`
	URL       string   `json:"url"`
	PostRefs  []string `json:"post_refs,omitempty" table:"-" kit:"link,kind=producthunt/post"` // posts they made
}

User is a Product Hunt member, emitted by user. PostRefs is the posts they made.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL