Changelog
Source:NEWS.md
pagerankr 0.1.0
Released 2026-07-11.
Documented that
canonical_profile()deliberately leavesrurl’s component-dropping knobs unpinned (query_handling,port_handling, and theurl_standardselector added inrurl2.2.0) – they have no effect on the scheme+host+path node key at their defaults. Added a behavioral guard intest-canonicalization.Rasserting a canonical key drops the port, query, and fragment, so a futurerurldefault flip on an unpinned knob is caught here rather than silently changing node identity. No node keys change; verified againstrurl2.2.0.canonical_profile()now pinspath_normalization = "dot_segments"andpath_encoding = "decode"(previously"none"/"keep").rurl2.1.0 silently redefined those two default values to keep the path verbatim, which changed node keys for any URL with dot-segments (/a/../b) or percent-encoding (/%41,%20) and desynced the pagerankr <-> semantic node join. Pinning the explicit values restores the original committed key (path percent-decoded, dot-segments removed) and keeps node identities stable across therurlupgrade. Thesemanticsibling pins the identical profile (changed together).clean_url_columns()now preserves tokens thatrurlcannot parse as a URL (e.g. a dotless bare label such as"A") as their raw value instead of turning them into NA. Newerrurl(>= 2.1.0) normalizes such dotless tokens to NA; combined with therurlfloor bump in the follow-up, this had silently collapsed non-URL node identities to NA — they were then dropped byget_unique_edges(), so [pagerank()] returned an empty result for any graph built from bare labels. Unparseable-but-present tokens are now kept as opaque nodes (only genuinely missing NA inputs stay NA), mirroring.apply_fold_map()’s leave-unmapped-values-untouched contract.filter_links_by_domain()’s encoding-independent registrable-domain matching now readsrurl’s newdomain_asciicolumn (rurl>= 2.1.0) instead of a separate IDNA-forced parse..build_url_maps()parses each unique URL once (host +domain_ascii) rather than twice, and the.domain_profile()forced-idna helper is gone. Behavior is unchanged —münchen.deandxn--mnchen-3ya.destill fold to one key under everyhost_encoding.pagerank_screaming_frog()gainsapply_canonicalsandapply_redirectstoggles (bothTRUEby default, preserving current behavior). Setting either toFALSEskips folding the bundle’s canonical / redirect signals into [pagerank()] (passescanonicals_df/redirects_dfasNULL), giving a supported as-crawled run that keeps the crawled node identities — the escape hatch for crawls whose canonicals point off the crawled domain (mirror / staging hosts) and would otherwise relabel crawled pages onto uncrawled targets. The reserved-argument guard still blocks the rawcanonicals_df/redirects_dfpagerank arguments.screaming_frog_bundle()now reports an off-domain canonical count (counts$canonicals_off_domain, surfaced insummary()/print()), reusing the existing absent-target classification, and the wrapper exposes it on thescreaming_frog_importaudit so the mirror-staging scenario is visible at import and scoring time.pagerank()now detects fold-target collisions: when a canonical/redirect relabels a crawled page’s node onto an uncrawled URL that is ALSO independently referenced as a genuine link endpoint, the two silently merge into one vertex and the crawled page absorbs the inbound link equity of that uncrawled URL.pagerank()emits awarning()naming the merged URL(s) and records them in thefoldsection of thetransition_auditobject under a newcollisionsfield (a data frame oftarget/n_independent_refs/source, orNULLwhen none). The crawl’s known-URL set (indexability_df) is used to tell an uncrawled fold target from a genuinely crawled leaf page, so the diagnostic is only computed when anindexability_dfis supplied.pagerank()now warns when akeep_domains/exclude_domains/keep_hosts/exclude_hostsvalue matched the crawled input but no node after folding — i.e. an out-of-scope canonical/redirect rewrote the crawled domain/host away before filtering (which runs after folding). The warning names the folded-away value(s) and points at the fold as the cause. The fold-then-filter ordering is now documented explicitly in thepagerank()andfilter_links_by_domain()docs; to scope the crawled input, filter withfilter_links_by_domain()before callingpagerank().out_of_scope_foldgains a third policy,"leak": a crawled page whose canonical/redirect folds out of scope is treated like an external redirect — its inbound equity is routed onto a dedicated leak sink and evaporates out of the measured graph (its outbound edges are dropped), so it does not rank and its equity is not credited to any surviving page. The evaporated equity is reported as a newleakedterm in thetransition_auditmassaccounting, which now decomposes asreported + sink + leaked + hidden = total(= 1);leakedis0for"relabel"/"keep"runs so their totals are unchanged. Thefoldaudit section reportspolicy == "leak".pagerank()gains anout_of_scope_foldargument ("relabel"default, or"keep") governing composed fold-map entries whose target is not itself a crawled node."relabel"preserves current behavior (fold crawled sources onto uncrawled canonical/redirect targets);"keep"drops those out-of-scope entries before folding so crawled pages retain their as-crawled identity rather than being relabeled to phantom vertices (the same filtered map folds the TIPR prior). Regardless of policy, the count and list of out-of-scope folds (source, target, signal) are recorded in a newfoldsection of thetransition_auditobject.Code-quality pass:
anyNA()replacesany(is.na()),!all(x)replacesany(!x), nestedifelse()replaced with vectorized assignment,expect_gt()/expect_lt()/expect_length()/expect_null()adopted where applicable, and redundantc()wrappers around single-string aliases removed.pagerank()andcompute_pagerank()gain convergence controls and reporting. The newalgoargument selects theigraph::page_rank()back-end ("prpack", the fast exact default, or"arpack", the iterative solver), and the friendlyeps/niteraliases re-introduce the L1 tolerance and maximum iteration count that modernigraphdropped, mapping onto the ARPACKoptions$tol/options$maxiter; supplying either transparently switches to ARPACK. Every non-empty result now carries a"convergence"attribute (apagerank_convergenceobject) reporting the solver, iteration count (when the solver exposes it), and a solver-independent post-hoc L1 residual||Gx - x||_1of the returned vector — a genuine quality check comparable across both back-ends. Docs cover the damping/iteration-count rule of thumblog10(eps) / log10(damping).New
topic_feeder_pagerank()andfeeder_seed_prior()answer the inverse oftopic_sensitive_pagerank(): not “which page is most authoritative for this cluster” but “which pages feed / power this cluster” — the internal hubs whose outlinks point into the target pages. It seeds the teleport prior on the cluster and runspagerank()on the transposed graph (reverse = TRUE), so mass walks backward along links and accumulates on the feeders, attenuating with link distance. This is the reverse-graph sibling of Topic-Sensitive PageRank and the cluster-biased counterpart to the global inverse PageRank (pagerank(reverse = TRUE)); the feeders are the high-pagerankrows withprior_weight == 0(cluster pages carry teleport mass directly). Pure orchestration over the existing TIPR personalization path on the reversed graph — no new solver. Newtopic_feeder_pagerankvignette walks through the AI-Agent-cluster use case and contrasts it with the forward authority view and with HITS hubs.New
salsa()andcompute_salsa()add Lempel & Moran’s (2001) SALSA hub and authority scores: a stochastic variant of HITS that runs the mutual-reinforcement step as PageRank-style random walks on the bipartite hub/authority graph, so the scores are stationary distributions rather than dominant eigenvectors. Computed over the same cleaned, redirect/canonical-folded, domain-filtered, deduplicated link graph aspagerank(), so hub, authority, and PageRank share node identities. Uses the degree-based closed form (Proposition 6) — no eigenvector iteration — with the required weakly-connected-component mass-weighting correction so cross-component scores stay comparable on crawls with orphan clusters. Each side sums to 1; coverage differs from PageRank by design (hubisNAfor pure sinks,authorityisNAfor pure sources). v1 is unweighted; a weighted extension is deferred. Documented as a site-graph adaptation of the original focused-subgraph algorithm.New
trustrank()andtrust_seed_prior()add TrustRank-style seed-biased PageRank (Gyöngyi, Garcia-Molina & Pedersen, 2004): personalized PageRank whose teleport vector is concentrated on a set of trusted seed pages, so trust flows outward from the seeds and attenuates with distance (the damping factor is the attenuation).trust_seed_prior()builds aprior_dffrom a seed set (character vector or weighteddata.frame; equal weights reproduce the original uniform seed distribution), andtrustrank()is the one-call wrapper that builds the seed prior and runspagerank()with it. Pure orchestration over the existing TIPR personalization path — no new solver; seed selection is the caller’s (it is seed-biased PageRank, not a spam classifier). Newtrustrankvignette walks through a worked example.New
topic_sensitive_pagerank()computes per-topic PageRank by running the existingpagerank()engine once per topic with a teleport prior biased toward each topic’s seed cluster, then blends the per-topic scores into a single ranking. This is Haveliwala’s (2002) Topic-Sensitive PageRank adapted to a single site: each “topic” is a content cluster (e.g. the pricing or AI-Agent area) given as a character vector of seed URLs or a weighteddata.frame. Returns one score column per topic plus a weight-normalizedblendedcolumn, with the per-topictransition_auditobjects attached. Pure orchestration over the TIPR personalization path — no new solver, and topic membership is supplied by the caller, not inferred.New
smooth_transitions()shrinks sparse empirical page-transition shares (e.g. fromga4_page_transitions()) toward the crawl-graph link structure, so no valid crawled link is ever assigned exactly zero probability. Uses a per-source Dirichlet/pseudocount shrinkage weightlambda_i = n_i / (n_i + k)that increases with the source page’s sample size, withmin_supportfallback to the prior, optional weighted priors, and anorigindiagnostic (both/empirical_only/structural_only). Time decay and device/template/channel segmentation are handled upstream by shaping the count input.New
hits()andcompute_hits()add Kleinberg’s HITS hub and authority scores, computed withigraph::hits_scores()over the same cleaned, redirect/canonical-folded, domain-filtered, deduplicated link graph aspagerank(), so hub, authority, and PageRank share node identities. Docs cover the matrix formulation (authority = dominant eigenvector ofA^T A, hub = dominant eigenvector ofA A^T) and the whole-graph caveat: unlike Kleinberg’s query-focused base set, these are site-wide structural centralities.pagerank()now has an explicitduplicate_edge_policyfor repeatedfrom -> torows after URL folding and filtering. The default"collapse"preserves the standard binary/destination-level PageRank convention and previous results; opt-in"aggregate"sums duplicate numeric weights withaggregate_edges()semantics; opt-in"count_instances"models a link-slot surfer where repeated links increase transition probability and records instance-count details in the transition audit.Recorded representative Screaming Frog crawl acceptance results and added package-level operational documentation for required exports, default graph policy, optional origin/placement policies, and contract pinning.
New
pagerank_screaming_frog()scores ascreaming_frog_bundle()through the existingpagerank()pipeline, feeding only graph-eligible hyperlink edges while attaching Screaming Frog import diagnostics beside the transition audit. Placement/origin filtering and placement-derived weighting are explicit opt-ins.New
screaming_frog_bundle()composes Internal: All and All Inlinks/Outlinks adapters into the stable crawl handoff object with raw observations, graph edges, node signals, cross-table reconciliation diagnostics, provenance, and concise print/summary methods.New canonical and composed URL-resolution helpers:
resolve_canonicals(),resolve_canonical_urls(), andresolve_folded_urls()expose the existing fold-map engine for rel=canonical and redirect+canonical URL folding without duplicating resolver logic.Documented the indexed-corpus assumption used by
pagerank(): noindex pages may receive authority but their outlinks are treated as nofollow for propagation within the indexed graph. The docs now distinguish slot-consuming"evaporate", slot-removing"drop", and normally followed"keep"without attributing this package model to Google.New
screaming_frog_links()imports All Inlinks and All Outlinks with identical Source-to-Destination orientation, preserving raw duplicate observations while deriving explicit Hyperlink-only graph edges with nofollow, placement, origin, endpoint, and exclusion diagnostics.New
screaming_frog_internal()imports UTF-8/BOM Internal: All exports with alias-insensitive schema detection and selective file reads. It returns deterministic node, redirect, canonical, and indexability tables while preserving raw URLs and reporting missing, duplicate, invalid, and ignored input facts.pagerank()now attaches atransition_auditprovenance object to its result asattr(result, "transition_audit")(backward-compatible): row/edge counts, behavioral-weight coverage, normalization total, dropped data (NA / dedup / self-loop rows, unmatched prior URLs), robots-blocked count, and the model configuration used. Has aprintmethod.The
transition_auditobject’smassfield now decomposes the page-mass deficit precisely intoreported(visible page mass),sink(evaporated nofollow-sink mass),hidden(robots-blocked mass), andtotal(= 1 by construction) — replacing undifferentiated “leakage” language with precise evaporated/hidden accounting.New
aggregate_edges(): loss-aware post-fold edge aggregation with explicit per-column semantics (sum counts, boolean conflict policyany/all/majority/error,preserve_colslist-columns for placement features).New
transform_edge_weights(): per-source grouped weight transforms reusingtransform_weights()methods, emitting a per-sourcetransition_probabilitythat sums to 1 within eachbygroup.New
ga4_page_transitions(): consecutive-page-view transition counts from a GA4 BigQuery export with a deterministic session/event ordering contract (timestamp + batch tie-breaks). A transition signal, not a link-click signal.New
ga4_entrance_teleport(): entrance/landing-page counts as a teleport (reset) vector reusing theprior_df/align_prior_to_vertices()machinery; documented as a proxy, distinct from the backlink-authority prior.Initial CI and lint baseline.