library(tidyverse) # Data wrangling
library(rvest) # HTML scraping
library(httr2) # HTTP requests with user-agent control
library(kableExtra) # Formatted tables
Following the same steps as the in-class SEO demo. Using
read_html(), html_element() /
html_elements(), html_text2(), and
str_squish() this notebook scrapes the
<title> tag, meta description, and H1/H2/H3 headings
from two sites I chose: Target.com and
IKEA.com.
I played with the colors a bit as well. Both red and blue as
background/font combos were too difficult to read. I used the logo
colors of Target and IKEA for the font and background of this report:
red and blue for font and white for the background which made it more
readable.
Code was produced using AI.
sites <- tibble(
source = c("Target", "IKEA"),
url = c("https://www.target.com", "https://www.ikea.com/us/en/")
)
# Scrapes title, meta description, and H1/H2/H3 headings from one URL
scrape_seo_elements <- function(url, source_name) {
Sys.sleep(1.5) # Polite delay between requests
tryCatch({
resp <- request(url) %>%
req_headers(
`User-Agent` = paste0(
"Mozilla/5.0 (compatible; MSBA580-Research-Bot/1.0; ",
"+https://github.com/utjimmyx)"
)
) %>%
req_timeout(15) %>%
req_perform()
page <- resp %>%
resp_body_string() %>%
read_html()
title <- page %>%
html_element("title") %>%
html_text2() %>%
str_squish()
meta_desc <- page %>%
html_element("meta[name='description']") %>%
html_attr("content") %>%
str_squish()
h1 <- page %>% html_elements("h1") %>% html_text2() %>% str_squish()
h2 <- page %>% html_elements("h2") %>% html_text2() %>% str_squish()
h3 <- page %>% html_elements("h3") %>% html_text2() %>% str_squish()
tibble(
source = source_name,
url = url,
title = if (length(title) == 0 || is.na(title)) NA_character_ else title,
meta_description = if (length(meta_desc) == 0 || is.na(meta_desc)) NA_character_ else meta_desc,
h1_count = length(h1),
h2_count = length(h2),
h3_count = length(h3),
h1_sample = paste(head(h1, 3), collapse = " | "),
h2_sample = paste(head(h2, 5), collapse = " | "),
h3_sample = paste(head(h3, 5), collapse = " | ")
)
}, error = function(e) {
# Reason is printed to the knitted output (not suppressed) so a failed
# scrape is visible instead of silently turning into unexplained NAs.
warning("Could not scrape ", source_name, ": ", conditionMessage(e), call. = FALSE)
tibble(
source = source_name, url = url, title = NA_character_,
meta_description = NA_character_, h1_count = NA_integer_,
h2_count = NA_integer_, h3_count = NA_integer_,
h1_sample = NA_character_, h2_sample = NA_character_, h3_sample = NA_character_
)
})
}
seo_results <- map2_dfr(sites$url, sites$source, ~ scrape_seo_elements(.x, .y))
seo_results %>%
select(source, title, meta_description) %>%
kbl(caption = "Title Tags and Meta Descriptions") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
| source | title | meta_description |
|---|---|---|
| Target | Target : Expect More. Pay Less. | Shop Target online and in-store for everything from groceries and essentials to clothing and electronics. Choose contactless pickup or delivery today. |
| IKEA | Shop Affordable Home Furnishings & Home Goods - IKEA | Find affordable furniture and home goods at IKEA! Discover furnishings and inspiration to create a better life at home. Shop online or in store! |
seo_results %>%
select(source, h1_count, h2_count, h3_count) %>%
kbl(caption = "Heading Tag Counts") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
| source | h1_count | h2_count | h3_count |
|---|---|---|---|
| Target | 1 | 14 | 80 |
| IKEA | 1 | 17 | 32 |
seo_results %>%
select(source, h1_sample, h2_sample, h3_sample) %>%
kbl(caption = "Sample Heading Text") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE)
| source | h1_sample | h2_sample | h3_sample |
|---|---|---|---|
| Target | Homepage | Endless summer style | Refresh your summer beauty routine | Discover 1000+ new eats & sips | Just in for summer | LoveShackFancy x Target | Loading… | Loading… | Loading… | Loading… | Loading… |
| IKEA | Welcome to IKEA USA | College starts soon – save now for great sleep all semester | College must-haves at must-have prices | Up to 20% off college essentials in-store only thru 7/21 | Delicious deals on your favorite foods | Today’s best deals | Sign in to IKEA Family or IKEA Business Network to save | 50% off entrées for college students | 50% off meatballs every Thursday | Kids eat free on Wednesdays | Last chance |
Several other sites I first tried (Amazon, Etsy, Best Buy, Costco) block automated requests with bot-detection (HTTP 403 errors or dropped connections), which is itself a relevant SEO/crawlability observation. A site that blocks well-behaved crawlers risks blocking search engine bots too if misconfigured. Target and IKEA both responded normally.
IKEA’s homepage shows a more complete on-page SEO structure than
Target’s: both carry a single <h1> and a clear,
benefit-driven meta description, but IKEA returns 32 real,
content-bearing <h3> tags (promotions, membership
prompts, deals) compared to Target, whose <h3>
elements are almost entirely “Loading…” placeholders because Target
renders that content client-side with JavaScript that a static scraper
cannot execute. Both sites have similarly rich <h2>
sections (17 for IKEA, 14 for Target) built around seasonal promotions.
Because search engine crawlers behave much like this static scraper
(they see the initial HTML, not everything JavaScript renders afterward)
IKEA’s static markup is giving Google more usable heading text to index
than Target’s, making IKEA the stronger performer on these specific SEO
elements.
Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach. O’Reilly Media. https://www.tidytextmining.com/
Xu, J.Z. (2026). Text Analysis & NLP 3 - Word Co-Occurrences for SEO. MSBA 580 Course Materials.