Repository navigation
Releases: rasuvaeff/link-preview
Releases · rasuvaeff/link-preview
Release list
v1.0.0
- Stable release on
rasuvaeff/schema^1.0 andrasuvaeff/json-ld^1.0
(rasuvaeff/pinned-http-client^1.0 suggested). The public API and
behaviour are those of 0.3.0, checked in the cska-one pilot; from here on
SemVer applies strictly. - Docs:
siteNamefalls back toproviderNamewhether or not
hostAsSiteNameis on (the 0.3.0 note said otherwise).
v0.3.0
Findings of the cska-one consumer pilot:
siteNamefalls back toproviderNamebefore the host (hostAsSiteName):
a card known only by its oEmbed provider is no longer signed by its domain.- A pinning client's own body limit (
rasuvaeff/pinned-http-client
ResponseTooLarge) isTooLarge, notClientRefused— the two limits no
longer need the "+1 byte" dance to tell a big body from a refusal. fetchMedia(): a 2xx answer with an empty body is the new
FetchFailureReason::EmptyBody/PreviewFailureReason::EmptyBody
(permanent) instead of an empty success.- An unresolvable host from pinned-http-client 0.3 (
ResolutionFailed, a
NetworkExceptionInterface) isNetwork— transient; dev/suggest bumped
to^0.3. - Requires
rasuvaeff/schema^0.6 (internal answer schemas; diagnostics now
quote non-plain keys in paths).
v0.2.0
- Requires
rasuvaeff/json-ld^0.2 (ProcessingOptions; loader failures arrive as
DocumentLoadingException). No change in what is read.
The pre-1.0 contract: what 1.0 will freeze. Breaking changes are marked BC:.
- BC: failures are value objects.
PreviewProvider::preview()/fromHtml()returnResult<PageInfo, PreviewFailure>wherePreviewFailureis now a class withreason(the new enumPreviewFailureReason),status(the HTTP status of anHttpStatusfailure),retryAfterSeconds(the server'sRetry-After: delta-seconds, or an HTTP-date counted from the response'sDate; capped at a day) andisTransient()(network, spent budget, HTTP 408/425/429/5xx). TheFetcherdoes the same withFetchFailure/FetchFailureReason. Migration:$result->error() === PreviewFailure::Networkbecomes$result->error()?->reason === PreviewFailureReason::Network(the old form now fails loudly: the constants are gone);->error()?->valuebecomes->error()?->reason->value; retry only whenisTransient(). - BC:
NotHtmlis renamedUnexpectedContentType(unexpected_content_type) in both reason enums - it is also whatfetchJson()andfetchMedia()answer. Migration: rename; storednot_htmlcodes map tounexpected_content_type. - BC: a client exception that is not a
NetworkExceptionInterfaceis the new, permanentClientRefused(wasNetwork): a pinning client refusing a forbidden address must not be retried. Migration: handleClientRefusedwhere you handledNetworkfor non-network errors. - BC:
PageInfo::$urlis the fetched URL (after redirects, policy-normalised) and nothing else; what the page claims (og:url,<link rel="canonical">, JSON-LDurl, an adapter's URL) moves to the newPageInfo::$canonicalUrl, so a page cannot make a card point at another host. Migration: link tourlas before; readcanonicalUrlwhere you relied on the page's own address (deduplication). - BC: text is clean.
Limits(and the merge, so markup-only values fall through to the next source) decodes HTML entities repeatedly (&quot;->"), replaces tags, comments and doctypes with a space, collapses control characters and whitespace, and only then clips; text escaped more than four levels deep keeps its&/<as full-width&/<. Applies totitle,description,siteName,authorName,providerName. Migration: drop your own decoding/stripping (it would now double-decode); still escape on output. - BC:
fromHtml()takes itspageUrlthrough the URL policy: a relative, private or non-http(s) one isErr(PolicyRejected)instead of an exception, andPageInfo::$urlis its normalised form. Its parameter isstring(wasnon-empty-string). Migration: handle theErr; pass an allowed URL. - BC: the eleven concrete adapters (
YouTubeAdapter, ...,RutubeAdapter) andExtract\Schemasare@internal, so neither their constructors norrasuvaeff/schematypes are part of the contract. Migration: get the adapters fromAdapterRegistry::standard(...); put your own in front with the newAdapterRegistry::with(Adapter ...$adapters)(first match wins, so yours override a built-in for the same domain); build test answers from your own fixtures instead ofSchemas::*()->arbitrary(). - BC:
PageInfo::toArray()has four more keys (canonicalUrl,imageWidth,imageHeight,providerName; fifteen in all) andExtract\Metadatathree more constructor parameters (imageWidth,imageHeight,providerName, appended). Migration: compare arrays by key, not by count. PageInfo::$imageWidth/$imageHeight: the declared size of the chosen image, fromog:image:width/height,twitter:image:width/height, the<img>of the HTML fallback, oEmbedthumbnail_width/height(a photo'swidth/height) and a JSON-LDImageObject(number, digits orQuantitativeValue). The size travels with its image through the merge andLimits.PageInfo::$providerName: oEmbedprovider_name, or the site adapter (Wikipedia,GitHub,Telegram,VK,Rutube).PageInfo::fromArray(): the inverse oftoArray()for caches and databases; missing keys arenull(0.1 arrays still read), wrong types throwInvalidArgumentException.LinkPreviewer: the interface of the facade (preview(),fromHtml()), implemented byPreviewProvider, for cache / rate-limit decorators (examples/cached_previewer.php).Fetcher::fetchMedia(string $url, list<string> $acceptedTypes, int $maxBytes): a bounded download (an image of a preview) under the same policy, redirect checks andUser-Agentas a page;type/subtypeortype/*, its own size limit.UrlPolicy::allowsAddress(string $address): judges a resolved IP exactly ascheck()judges a URL with that IP; it is theAddressPolicyofrasuvaeff/pinned-http-client(README Usage).PreviewProvider(hostAsSiteName: true): opt-insiteNamefallback to the host ofurlwithoutwww.(ASCII). Off by default so that a storedsiteNameis always something a source said.- Docs:
PinnedClientis the client of the quickstart (README, llms.txt, skill); the failure taxonomy is a README table; new examplesfailures.php,cached_previewer.php,media.php;with_pinned_client.phpandlive.phpuserasuvaeff/pinned-http-client^0.2 (dev and suggest constraint raised from ^0.1).
v0.1.2
- Requires
rasuvaeff/schema^0.5: the open answer schemas are declared with
unknown: Unknown::Passthrough(wasadditionalProperties: true); parsing
and the parsed arrays are unchanged. - CI: the property regression corpus is restored before and saved after the
coverage run, so a falsified property replays on later runs. - Tests: the broken-markup fixture no longer puts a tag inside
<title>—
libxml 2.14+ (theubuntu-26.04runners) reads the title as RCDATA, as
HTML5 does, so the expected text depended on the libxml version; the
clipping property draws blank text as its own branch, so its 1% "blank"
coverage gate no longer flakes.
v0.1.1
- DNS pinning is now
rasuvaeff/pinned-http-client(suggested, dev-required for the example):examples/with_pinned_client.phpfeeds it the rules ofUrlPolicy; README and agent skill point to it instead of a recipe.
v0.1.0
examples/pinned_client.php: a PSR-18 client over Guzzle that pins the connection to the addresses checked byUrlPolicy(the one thing the package leaves to the application), with a README paragraph.- JSON-LD is read by
ProcessorJsonLdReaderon top of the new dependencyrasuvaeff/json-ld(JSON-LD 1.1 expansion): inline contexts, aliases and compact IRIs work; the schema.org context is answered from memory (SchemaOrgContextLoader, internal), any other remote context skips the block, types and properties outside schema.org are dropped. Same previews as the former reader on 14 real-page blocks. UserAgentMap::unfurler()(andUserAgentMap::UNFURLER): the unfurler nameTwitterbot/1.0for every host with per-host overrides; checked live that VK and championat.com refuse the default name.UrlPolicy: http(s)-only, port, public-IP and IDN policy with CIDR allowlist, no DNS resolution.Fetcher: PSR-18 fetching with its own redirect loop (policy on every hop), redirect limit, between-hop budget, response size and content-type limits, per-hostUserAgentMap.PageDocument: read-only DOM view with charset detection.OpenGraphExtractor,TwitterCardsExtractor,HtmlFallbackExtractorreturningResult.PageInfo,Limits,PreviewProviderfacade with fixed-priority merge.OEmbedExtractor: oEmbed discovery; the endpoint a page names is fetched under the sameUrlPolicyand redirect checks as the page.JsonLdExtractorwith a replaceableJsonLdReader(defaultProcessorJsonLdReader; nothing remote is ever loaded).Fetcher::fetchJson()andFetcher::check(); custom request headers go to the first host only.- Site adapters behind
Adapter/AdapterRegistry: YouTube, TikTok, Twitter/X, Twitch, Wikipedia, GitHub, Instagram, Facebook, Telegram, VK, Rutube. Hosts are matched label by label; Instagram and Facebook require an access token and fail withMissingTokenwithout one. - JSON answers of oEmbed, Wikipedia and Rutube are parsed with
rasuvaeff/schema(Extract\Schemas);AdapterContext::diagnostics()reports unreadable shapes without values. PageInfo::$embedUrl;PreviewFailure::MissingToken.- Recorded adapter corpus in
tests/fixtures/adapters/,bin/record-preview-corpus, env-gated live integration suite.