Presets: named views of the link graph
Bart Turczynski
2026-09-21
Source:vignettes/presets.Rmd
presets.RmdWhy presets exist
PageRank is a lens, not the deliverable. The same crawl answers different questions depending on how you prepare the graph first: what does the site look like exactly as crawled? What does it look like once its own canonical and nofollow declarations are honored? Which pages feed authority outward rather than collect it? What survives once site chrome stops voting?
pagerank() can express all of those, but only through a
long argument list. A preset is a small named bundle of
those arguments describing one recurring view, so a view becomes a
one-liner:
edges <- data.frame(
from = c("A", "A", "B", "C"),
to = c("B", "C", "A", "A")
)
pagerank(edges, preset = "raw")
#> node_name pagerank
#> 1 A 0.4864865
#> 2 B 0.2567568
#> 3 C 0.2567568Presets are opt-in. pagerank() with no
preset behaves exactly as it always has — nothing here
changes a default.
The four presets
pr_preset("raw")
#> $self_loops
#> [1] "keep"
#>
#> $drop_isolates_flag
#> [1] FALSE
#>
#> $nofollow_action
#> [1] "keep"
#>
#> $out_of_scope_fold
#> [1] "keep"
#>
#> attr(,"pr_preset_name")
#> [1] "raw"A preset is a plain named list, so you can always print one to see exactly what it does. Nothing is hidden: the table below is the whole registry.
| Preset | Expands to | The view |
|---|---|---|
"raw" |
self_loops = "keep",
drop_isolates_flag = FALSE,
nofollow_action = "keep",
out_of_scope_fold = "keep"
|
The graph exactly as crawled. Nothing is applied;
rel=nofollow votes like any other link. The faithful
baseline. |
"declared" |
self_loops = "drop",
drop_isolates_flag = TRUE,
nofollow_action = "evaporate",
robots_blocked_action = "show",
out_of_scope_fold = "relabel"
|
The graph after honoring what the site declares — nofollow, canonicals, redirects, robots. A pure pin of today’s defaults. |
"reversed" |
reverse = TRUE |
Every edge flipped: a page scores highly when it points at well-connected pages. The feeder view. |
"content" |
placement_weights = c(content = 1, nav = 0.1, header = 0.1, footer = 0.1, aside = 0.1) |
Links in the main content keep their full vote; links in site chrome are discounted to a tenth. |
"raw" — the faithful baseline
The naive view, and deliberately the one the package makes easiest to
state. Every other view is a claim about what should count;
"raw" is the claim-free comparison point. Reach for it when
you want to see how much of a ranking your own preparation choices
produced.
"declared" — a pin, not a change
"declared" sets every value to what
pagerank() already defaults to. Running with it produces
byte-identical scores to running without it:
That is the point. Its value is provenance: a run made with
preset = "declared" is a recorded, auditable statement that
the declared view was intended, and it stays pinned to the bundle as
documented even if a future package default moves.
Note the boundary: a preset sets policy, never data.
"declared" turns on honoring canonicals and
redirects, but the canonical and redirect tables themselves
(canonicals_df, redirects_df,
indexability_df) are still yours to supply.
"reversed" — the feeder view
pagerank(edges, preset = "reversed")
#> node_name pagerank
#> 1 A 0.4864865
#> 2 B 0.2567568
#> 3 C 0.2567568topic_feeder_pagerank() already reverses the graph
itself, so preset = "reversed" is a harmless no-op there
rather than an error.
"content" — stop letting the navigation rank the
site
Site chrome — navigation, header, footer, sidebar — is typically the large majority of a crawl’s edges. Left unweighted it does not merely influence the ranking, it manufactures it: every page links to the same handful of chrome targets, so those targets win by sheer repetition regardless of editorial intent.
"content" discounts them. It needs one thing from you:
placement_col, the name of a column saying which page
region each link was found in. That is data, so the preset cannot supply
it.
placed <- data.frame(
from = c("A", "A", "B", "C"),
to = c("B", "C", "A", "A"),
region = c("content", "footer", "content", "content")
)
pagerank(placed, preset = "content", placement_col = "region")
#> node_name pagerank
#> 1 A 0.48648649
#> 2 B 0.42592138
#> 3 C 0.08759214B and C are each linked once from
A. Unweighted they tie; weighted, the footer link is worth
a tenth of the in-content one and C falls behind.
The accepted region terms are content, nav,
header, footer and aside.
pagerank_screaming_frog() derives them for you from the
crawl, so there preset = "content" works with no extra
argument.
Two properties worth stating explicitly:
-
All five terms are named. Regions absent from
placement_weightskeep weight 1, which is what makes a partial recipe likec(nav = 0.1)mean “discount nav, leave everything else alone”. A preset is a complete recipe, so it names every term — otherwise footer and aside would quietly outweigh nav tenfold. - Edges are downweighted, never dropped. Region detection is a heuristic, so a misclassified content link at 0.1 is a small error where a dropped one is a silent deletion. Dropping most of a graph also manufactures isolates and dangling pages, at which point you are measuring your own sink policy rather than the site.
Layers vs. presets: raw, real, and annotated
A preset bundles policy — the graph-hygiene and
weighting knobs above. It never touches data: the
redirects_df, canonicals_df,
indexability_df and status_df tables you
supply (or leave NULL). Those tables decide a second,
orthogonal thing — how much of the crawl is folded away before
scoring — and that is its own view model, layered on top of whichever
preset you pick. Three layers, from the crawl as collected to the crawl
fully interpreted:
(a) Raw — every crawled URL is its own node. Nothing
is folded: a redirecting URL keeps the PageRank it receives instead of
passing it on, a canonicalized page stays separate from its canonical,
and a 404 scores like any live page. You get this by not
supplying the fold tables — they all default NULL, so
the raw layer is the absence of data. This is exactly why the
"raw" preset alone cannot guarantee it: a preset sets
policy, and no policy can un-supply a redirects_df you
passed. On the bare pagerank() path the raw layer is the
default (you supply no tables), and preset = "raw"
additionally relaxes the hygiene policy so nothing at all is applied. On
the Screaming Frog path, where the wrapper builds those tables from the
bundle for you, preset = "raw" is special-cased to switch
the folding back off — so it delivers the raw layer there too.
(b) Real — redirects and canonicals are followed; dead pages
collect but cannot pass. Supply the fold tables and the graph
reflects the live, canonical corpus a search engine would
score: redirect and canonical sources fold into their terminal target
(an alias, not a separate page), and the “collects PageRank but cannot
pass it” class — noindex, robots-blocked, and 4xx/5xx
response-dead pages — routes its throughput to a shared waste sink
instead of recirculating it (see ?pagerank and
vignette("pagerankr-usage") for why the sink is the only
faithful choice for a dead page). This is the view most internal-link
audits want; pagerank_screaming_frog() assembles it from
the crawl by default.
(c) Annotated — the page_state
indicator. Layer (b) hands a folded score to a page that may be
a black hole: a 404 can amass PageRank from every page still
linking to it, then evaporate it to the sink. That score is real mass
but not earned authority, and nothing else on the row distinguishes it
from a strong live page. So whenever indexability_df or
status_df is supplied, the result gains a
page_state column tagging each row live,
noindex, robots_blocked, or
response_dead (the highest-precedence signal wins). The
annotation is the package’s answer to “why is this dead URL ranking?” —
and the reason such rows can be kept in the output instead of
silently dropped.
Layers and presets compose freely: a layer is which data you fold, a preset is which policy you bundle, and you choose them independently.
Precedence: explicit argument > preset > default
A preset never silently overrides something you typed.
# The raw bundle says nofollow_action = "keep"; the explicit argument wins.
attr(
pagerank(edges, preset = "raw", nofollow_action = "drop"),
"transition_audit"
)$config$nofollow_action
#> [1] "drop"This holds through every wrapper that forwards ... to
pagerank() — trustrank(),
topic_sensitive_pagerank(),
topic_feeder_pagerank() and
pagerank_screaming_frog() — with one boundary: arguments a
wrapper sets itself are wrapper-owned, and a preset cannot
change those.
Presets are not composable with each other; preset takes
a single bundle. They do not need to be: "content" sets
only placement weights and leaves every hygiene knob at its default, and
the defaults are the "declared" view.
Inspecting and adapting a bundle
Because a preset is an ordinary list, you can splice it, print it, or use it as a starting point:
# Splice a bundle into a call
do.call(pagerank, c(list(edges), pr_preset("raw")))
#> node_name pagerank
#> 1 A 0.4864865
#> 2 B 0.2567568
#> 3 C 0.2567568
# Start from a preset and adjust
my_view <- pr_preset("raw")
my_view$drop_isolates_flag <- TRUE
pagerank(edges, preset = my_view)
#> node_name pagerank
#> 1 A 0.4864865
#> 2 B 0.2567568
#> 3 C 0.2567568Hand-rolled bundles are validated against pagerank()’s
formals, so a misspelled argument fails loudly instead of sliding
through ....
Provenance in the transition audit
The audit attached to every result records which named view produced it:
It records the preset name for a registered preset (whether passed by
name or as a pr_preset() result), "custom" for
a hand-rolled bundle, and NULL when no preset was used. Two
runs that expand to the same configuration are therefore still
distinguishable by the intent behind them — which, for a pure pin like
"declared", is the only durable record that intent
existed.
See also
-
?pr_presetfor the reference documentation. -
vignette("pagerankr-usage")for the underlying arguments each preset sets.