torus

Beyond Links: Understanding Page Transitions in Chrome

When SEOs think about user behavior, the conversation often revolves around clicks, links, and conversions. But in Chrome, there’s an underlying layer of data that tells a much richer story—page transitions. These are the bread and butter of how users navigate, revealing not just where they go, but how they got there.

For SEOs, understanding these transitions opens up new insights into intent, usability, and the real pathways users take beyond the usual attribution models.


What Are Page Transitions?

Page transitions in Chrome describe the types of navigational actions that users perform. Think of them as Chrome’s version of “user intent signals,” baked directly into how the browser logs movement from one page to another. These transitions are meticulously categorized into core types and qualifiers, offering a granular view of the motivations behind visits.

This data, when correlated with SERP performance or site analytics, can redefine how you interpret user journeys.


Page Transition Types and What They Mean for SEO

Here’s a breakdown of the core transition types, each with SEO implications:

1. PAGE_TRANSITION_LINK

  • What it means: The user clicked a hyperlink.
  • SEO angle: This is your bread-and-butter traffic—users moving through internal links, backlinks, or SERP results. A high percentage of these signals solid internal linking and/or good backlink acquisition.

2. PAGE_TRANSITION_TYPED

  • What it means: The user typed the URL into the browser.
  • SEO angle: Brand loyalty and awareness shine here. These transitions are golden for branded search. If users repeatedly type in your domain, it signals strong direct traffic that could buffer against algorithm changes.

3. PAGE_TRANSITION_AUTO_BOOKMARK

  • What it means: The user navigated via a bookmark.
  • SEO angle: Bookmark usage suggests recurring engagement, a sign of valuable, sticky content. If your blog posts or tools get bookmarked, it’s a great retention metric.

4. PAGE_TRANSITION_AUTO_SUBFRAME

  • What it means: Non-toplevel content (e.g., ads, embedded media) automatically loads.
  • SEO angle: Useful for understanding the visibility and impact of programmatic ad content or third-party embeds. It’s also a reminder to audit subframe content for speed and accessibility.

5. PAGE_TRANSITION_MANUAL_SUBFRAME

  • What it means: A user manually navigated within a frame, such as clicking a link in an iframe.
  • SEO angle: Rare but critical for pages relying on iframes (e.g., embedded tools or interactive widgets). This may hint at overlooked pathways users take on your site.

6. PAGE_TRANSITION_GENERATED

  • What it means: The URL was generated from user input (e.g., a search bar suggestion).
  • SEO angle: Think “user intent funneling.” If users end up here, your search features or site navigational suggestions are working well.

7. PAGE_TRANSITION_FORM_SUBMIT

  • What it means: The user submitted a form manually.
  • SEO angle: This is the holy grail of conversions. Forms that produce these transitions are where lead-gen efforts shine. It also highlights the value of well-optimized landing pages.

8. PAGE_TRANSITION_RELOAD

  • What it means: The user refreshed the page.
  • SEO angle: A high rate of reloads could signal usability issues (slow loads, broken content) or, conversely, highly dynamic, engaging content users want to revisit.

9. PAGE_TRANSITION_KEYWORD

  • What it means: A search keyword (non-default) triggered navigation.
  • SEO angle: Monitor this to understand alternative search providers and niche search behavior—critical in regions or markets where Google isn’t dominant.

10. PAGE_TRANSITION_KEYWORD_GENERATED

  • What it means: The browser generated a visit from a search query.
  • SEO angle: A reminder that browsers often act as intent mediators. Optimizing for semantic search and suggested queries can capture these users.

Qualifiers: Adding Depth to the Journey

Qualifiers refine these transitions, offering more detail. For instance:

  • PAGE_TRANSITION_BLOCKED: Blocked navigation by a managed user—relevant for SEO efforts in regulated industries.
  • PAGE_TRANSITION_FROM_API: Traffic from an external application—important for tracking app-referrals or API-driven links.

Note: Article edited for clarity and accuracy based on reader comments.

Source:

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef UI_BASE_PAGE_TRANSITION_TYPES_H_
#define UI_BASE_PAGE_TRANSITION_TYPES_H_

#include <stdint.h>

#include "base/component_export.h"

namespace ui {

// Types of transitions between pages. These are stored in the history
// database to separate visits, and are reported by the renderer for page
// navigations.
//
// WARNING: don't change these numbers. They are written directly into the
// history database, so future versions will need the same values to match
// the enums.
//
// A type is made of a core value and a set of qualifiers. A type has one
// core value and 0 or or more qualifiers.
//
// A Java counterpart will be generated for this enum.  This is why the enum
// uses int32_t and not uint32_t as the underlying type (jint cannot
// represent uint32_t).
// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.ui.base
enum PageTransition : int32_t {
  PAGE_TRANSITION_FIRST = 0,

  // User got to this page by clicking a link on another page.
  PAGE_TRANSITION_LINK = PAGE_TRANSITION_FIRST,

  // User got this page by typing the URL in the URL bar.  This should not be
  // used for cases where the user selected a choice that didn't look at all
  // like a URL; see GENERATED below.
  //
  // We also use this for other "explicit" navigation actions.
  PAGE_TRANSITION_TYPED = 1,

  // User got to this page through a suggestion in the UI, for example)
  // through the destinations page.
  PAGE_TRANSITION_AUTO_BOOKMARK = 2,

  // This is a subframe navigation. This is any content that is automatically
  // loaded in a non-toplevel frame. For example, if a page consists of
  // several frames containing ads, those ad URLs will have this transition
  // type. The user may not even realize the content in these pages is a
  // separate frame, so may not care about the URL (see MANUAL below). All
  // Fenced Frame navigations will be of this type because they are considered
  // a non-toplevel navigation that does not generate new navigation entries
  // in the back/forward list.
  PAGE_TRANSITION_AUTO_SUBFRAME = 3,

  // For subframe navigations that are explicitly requested by the user and
  // generate new navigation entries in the back/forward list. These are
  // probably more important than frames that were automatically loaded in
  // the background because the user probably cares about the fact that this
  // link was loaded.
  PAGE_TRANSITION_MANUAL_SUBFRAME = 4,

  // User got to this page by typing in the URL bar and selecting an entry
  // that did not look like a URL.  For example, a match might have the URL
  // of a Google search result page, but appear like "Search Google for ...".
  // These are not quite the same as TYPED navigations because the user
  // didn't type or see the destination URL.
  // See also KEYWORD.
  PAGE_TRANSITION_GENERATED = 5,

  // This is a toplevel navigation. This is any content that is automatically
  // loaded in a toplevel frame.  For example, opening a tab to show the ASH
  // screen saver, opening the devtools window, opening the NTP after the safe
  // browsing warning, opening web-based dialog boxes are examples of
  // AUTO_TOPLEVEL navigations.
  PAGE_TRANSITION_AUTO_TOPLEVEL = 6,

  // The user filled out values in a form and submitted it. NOTE that in
  // some situations submitting a form does not result in this transition
  // type. This can happen if the form uses script to submit the contents.
  PAGE_TRANSITION_FORM_SUBMIT = 7,

  // The user "reloaded" the page, either by hitting the reload button or by
  // hitting enter in the address bar.  NOTE: This is distinct from the
  // concept of whether a particular load uses "reload semantics" (i.e.
  // bypasses cached data).  For this reason, lots of code needs to pass
  // around the concept of whether a load should be treated as a "reload"
  // separately from their tracking of this transition type, which is mainly
  // used for proper scoring for consumers who care about how frequently a
  // user typed/visited a particular URL.
  //
  // SessionRestore and undo tab close use this transition type too.
  PAGE_TRANSITION_RELOAD = 8,

  // The url was generated from a replaceable keyword other than the default
  // search provider. If the user types a keyword (which also applies to
  // tab-to-search) in the omnibox this qualifier is applied to the transition
  // type of the generated url. TemplateURLModel then may generate an
  // additional visit with a transition type of KEYWORD_GENERATED against the
  // url 'http://' + keyword. For example, if you do a tab-to-search against
  // wikipedia the generated url has a transition qualifer of KEYWORD, and
  // TemplateURLModel generates a visit for 'wikipedia.org' with a transition
  // type of KEYWORD_GENERATED.
  PAGE_TRANSITION_KEYWORD = 9,

  // Corresponds to a visit generated for a keyword. See description of
  // KEYWORD for more details.
  PAGE_TRANSITION_KEYWORD_GENERATED = 10,

  // ADDING NEW CORE VALUE? Be sure to update the LAST_CORE and CORE_MASK
  // values below.  Also update CoreTransitionString().
  PAGE_TRANSITION_LAST_CORE = PAGE_TRANSITION_KEYWORD_GENERATED,
  PAGE_TRANSITION_CORE_MASK = 0xFF,

  // Qualifiers
  // Any of the core values above can be augmented by one or more qualifiers.
  // These qualifiers further define the transition.

  // The values 0x00200000 (PAGE_TRANSITION_FROM_API_3) and 0x00400000
  // (PAGE_TRANSITION_FROM_API_2) were used for experiments and were removed
  // around 6/2021. The experiments ended well before 6/2021, but it's possible
  // some databases still have the values. See https://crbug.com/1141501 for
  // more.

  // A managed user attempted to visit a URL but was blocked.
  PAGE_TRANSITION_BLOCKED = 0x00800000,

  // User used the Forward or Back button to navigate among browsing history.
  PAGE_TRANSITION_FORWARD_BACK = 0x01000000,

  // User used the address bar to trigger this navigation.
  PAGE_TRANSITION_FROM_ADDRESS_BAR = 0x02000000,

  // User is navigating to the home page.
  PAGE_TRANSITION_HOME_PAGE = 0x04000000,

  // The transition originated from an external application; the exact
  // definition of this is embedder dependent.
  PAGE_TRANSITION_FROM_API = 0x08000000,

  // The beginning of a navigation chain.
  PAGE_TRANSITION_CHAIN_START = 0x10000000,

  // The last transition in a redirect chain.
  PAGE_TRANSITION_CHAIN_END = 0x20000000,

  // Redirects caused by JavaScript or a meta refresh tag on the page.
  PAGE_TRANSITION_CLIENT_REDIRECT = 0x40000000,

  // Redirects sent from the server by HTTP headers. It might be nice to
  // break this out into 2 types in the future, permanent or temporary, if we
  // can get that information from WebKit.
  // TODO(crbug.com/40212666): Remove this as it's inaccurate.
  // NavigationHandle::WasServerRedirect() should be used instead.
  PAGE_TRANSITION_SERVER_REDIRECT = -2147483648,  // 0x80000000

  // Used to test whether a transition involves a redirect.
  PAGE_TRANSITION_IS_REDIRECT_MASK = -1073741824,  // 0xC0000000

  // General mask defining the bits used for the qualifiers.
  PAGE_TRANSITION_QUALIFIER_MASK = -256,  // 0xFFFFFF00
};

// Compares two PageTransition types ignoring qualifiers. |rhs| is taken to
// be a compile time constant, and hence must not contain any qualifiers.
COMPONENT_EXPORT(UI_BASE)
bool PageTransitionCoreTypeIs(PageTransition lhs, PageTransition rhs);

// Compares two PageTransition types including qualifiers. Rarely useful,
// PageTransitionCoreTypeIs() is more likely what you need.
COMPONENT_EXPORT(UI_BASE)
bool PageTransitionTypeIncludingQualifiersIs(PageTransition lhs,
                                             PageTransition rhs);

// Simplifies the provided transition by removing any qualifier
COMPONENT_EXPORT(UI_BASE)
PageTransition PageTransitionStripQualifier(PageTransition type);

COMPONENT_EXPORT(UI_BASE) bool IsValidPageTransitionType(int32_t type);

COMPONENT_EXPORT(UI_BASE) PageTransition PageTransitionFromInt(int32_t type);

// Returns true if the given transition is a top-level frame transition, or
// false if the transition was for a subframe.
COMPONENT_EXPORT(UI_BASE) bool PageTransitionIsMainFrame(PageTransition type);

// Returns whether a transition involves a redirection
COMPONENT_EXPORT(UI_BASE) bool PageTransitionIsRedirect(PageTransition type);

// Returns whether a transition is a new navigation (rather than a return
// to a previously committed navigation).
COMPONENT_EXPORT(UI_BASE)
bool PageTransitionIsNewNavigation(PageTransition type);

// Return the qualifier
COMPONENT_EXPORT(UI_BASE)
PageTransition PageTransitionGetQualifier(PageTransition type);

// Returns true if the transition can be triggered by the web instead of
// through UI or similar.
COMPONENT_EXPORT(UI_BASE)
bool PageTransitionIsWebTriggerable(PageTransition type);

// Return a string version of the core type values.
COMPONENT_EXPORT(UI_BASE)
const char* PageTransitionGetCoreTransitionString(PageTransition type);

// Ban operator== and operator!= as it's way too easy to forget to strip the
// qualifiers. Use PageTransitionCoreTypeIs() instead or, in rare cases,
// PageTransitionTypeIncludingQualifiersIs().
bool operator==(PageTransition, PageTransition) = delete;
bool operator==(PageTransition, int32_t) = delete;
bool operator==(int32_t, PageTransition) = delete;
bool operator!=(PageTransition, PageTransition) = delete;
bool operator!=(PageTransition, int32_t) = delete;
bool operator!=(int32_t, PageTransition) = delete;

}  // namespace ui

#endif  // UI_BASE_PAGE_TRANSITION_TYPES_H_

https://source.chromium.org/chromium/chromium/src/+/main:ui/base/page_transition_types.h


Comments

7 responses to “Beyond Links: Understanding Page Transitions in Chrome”

  1. Very interesting Dejan! Could you shine some light on how you capture this data from Chrome/Chromium?

    1. Certainly, for me this data is stored in: C:\Users\dejan\AppData\Local\Google\Chrome\User Data\Profile 1 folder. Yours will be slightly different based on your computer user and profile number in Chrome. One example is an sqlite database file called: “C:\Users\dejan\Desktop\chrome hacking\User 1\History” this is not a folder but a file.

      This is the script I use to inspect its content:

      import streamlit as st
      import sqlite3
      import csv
      from io import StringIO
      import math
      import pandas as pd
      import os

      def main():
      st.title("SQLite Database Browser")

      # Get list of valid database files in the directory
      base_dir = r'C:\Users\dejan\Desktop\chrome hacking\User 1'
      db_files = [
      file for file in os.listdir(base_dir)
      if os.path.isfile(os.path.join(base_dir, file)) and
      not file.endswith('-journal') and
      f"{file}-journal" in os.listdir(base_dir)
      ]

      # Let user select the database file
      selected_db = st.selectbox("Select Database File", db_files)

      if selected_db:
      db_path = os.path.join(base_dir, selected_db)
      st.write(f"Selected Database: **{selected_db}**")

      # Connect to the SQLite database
      conn = sqlite3.connect(db_path)
      cursor = conn.cursor()

      # Get list of tables in the database
      tables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table';").fetchall()
      tables = [table[0] for table in tables]

      # Display a summary table showing the number of records in each table
      summary_data = []
      for table in tables:
      count = cursor.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]
      summary_data.append({"Table Name": table, "Record Count": count})

      summary_df = pd.DataFrame(summary_data)
      st.write("**Summary of Tables:**")
      st.dataframe(summary_df)

      # Select table
      selected_table = st.selectbox("Select Table", tables)

      if selected_table:
      # Fetch and display schema for selected table
      schema = cursor.execute(f"PRAGMA table_info({selected_table});").fetchall()
      st.write(f"Schema for {selected_table} table:")
      st.write(schema)

      # Display summary for the selected table
      column_summary = []
      for col in schema:
      col_name = col[1]
      non_null_count = cursor.execute(f"SELECT COUNT({col_name}) FROM {selected_table} WHERE {col_name} IS NOT NULL").fetchone()[0]
      column_summary.append({"Column Name": col_name, "Non-Null Count": non_null_count})

      column_summary_df = pd.DataFrame(column_summary)
      st.write(f"**Summary for {selected_table} table:**")
      st.dataframe(column_summary_df)

      # User input for search query
      search_query = st.text_input("Search by text")

      # Modify SQL query based on search input
      query = f"SELECT * FROM {selected_table}"
      if search_query:
      columns = [col[1] for col in schema]
      search_conditions = " OR ".join([f"{col} LIKE '%{search_query}%'" for col in columns])
      query += f" WHERE {search_conditions}"

      # Fetch data
      data = cursor.execute(query).fetchall()

      # Pagination settings
      page_size = 100
      total_records = len(data)
      total_pages = math.ceil(total_records / page_size)
      page_number = st.number_input("Page number", min_value=1, max_value=total_pages, value=1)
      start_index = (page_number - 1) * page_size
      end_index = min(start_index + page_size, total_records)

      st.write(data[start_index:end_index])

      # Add button to download table data as CSV
      if st.button("Download Table as CSV"):
      csv_data = StringIO()
      csv_writer = csv.writer(csv_data)
      csv_writer.writerow([i[0] for i in cursor.description]) # Write headers
      csv_writer.writerows(data) # Write data rows
      csv_data.seek(0)
      csv_bytes = csv_data.getvalue().encode()
      st.download_button(label='Download CSV', data=csv_bytes, file_name=f'{selected_table}.csv', mime='text/csv')

      # Add button to delete table
      if st.button("Delete Table"):
      cursor.execute(f"DROP TABLE IF EXISTS {selected_table};")
      conn.commit()
      st.success(f"Table '{selected_table}' deleted successfully.")

      # Add button to empty table
      if st.button("Empty Table"):
      cursor.execute(f"DELETE FROM {selected_table};")
      conn.commit()
      st.success(f"Table '{selected_table}' emptied successfully.")

      # Close database connection
      conn.close()

      if __name__ == "__main__":
      main()

  2. PS: I recommend you copy the content of that whole folder to a separate location before you attempt loading it to prevent database locked message. Or deleting your history and breaking Chrome.

  3. Curious to know if you’ve looked at if Google could by sending this data across user sessions with Chrome Sync? Doing so could help with their conversion tracking for paid ads.

    1. I haven’t but I will!

  4. Sebastien Avatar

    Hi, very interesting article, that shows which signals may be used by Google. That said, how can this be a “treasure trove of insights” since we can’t access the page transition datas from users ?

    1. Valid point! It’s a treasure trove of insights for Google. I’ve edited the article to avoid suggesting we can get hold of this data (other than our own). Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *