diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web.Rmd b/_posts/2024-09-22-fetch-files-web/fetch-files-web.Rmd
similarity index 89%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web.Rmd
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web.Rmd
index 3c3ed003..0163f76b 100644
--- a/_posts/2024-09-01-fetch-files-web/fetch-files-web.Rmd
+++ b/_posts/2024-09-22-fetch-files-web/fetch-files-web.Rmd
@@ -1,7 +1,7 @@
---
title: 'Read files on the web into R'
description: |
- Mostly a compilation of some code-snippets for my own use
+ For the download-button-averse of us
categories:
- tutorial
base_url: https://yjunechoe.github.io
@@ -10,7 +10,7 @@ author:
affiliation: University of Pennsylvania Linguistics
affiliation_url: https://live-sas-www-ling.pantheon.sas.upenn.edu/
orcid_id: 0000-0002-0701-921X
-date: 09-01-2024
+date: 09-22-2024
output:
distill::distill_article:
include-after-body: "highlighting.html"
@@ -20,7 +20,6 @@ output:
editor_options:
chunk_output_type: console
preview: github-dplyr-starwars.jpg
-draft: true
---
```{r setup, include=FALSE}
@@ -36,7 +35,7 @@ knitr::opts_chunk$set(
Every so often I'll have a link to some file on hand and want to read it in R without going out of my way to browse the web page, find a download link, download it somewhere onto my computer, grab the path to it, and then finally read it into R.
-Over the years I've accumulated some tricks to get data into R "straight from a url", even if the url does not point to the raw file contents itself. The method varies between data sources though, and I have a hard time keeping track of them in my head, so I thought I'd write some of these down for my own reference. This is not meant to be comprehensive though - keep in mind that I'm someone who primarily works with tabular data and use GitHub and OSF as data repositories.
+Over the years I've accumulated some tricks to get data into R "straight from a url", even if the url does not point to the raw file contents itself. The method varies between data sources though, and I have a hard time keeping track of them in my head, so I thought I'd write some of these down for my own reference. This is not meant to be comprehensive though - keep in mind that I'm someone who primarily works with tabular data and interface with GitHub and OSF as data repositories.
## GitHub (public repos)
@@ -91,9 +90,9 @@ emphatic::hl_diff(
## GitHub (gists)
-It's a similar idea with GitHub Gists (sometimes I like to store small datasets for demos as gists). For example, here's a link to a simulated data for a [Stroop experiment](https://en.wikipedia.org/wiki/Stroop_effect) `stroop.csv`: Mostly a compilation of some code-snippets for my own useRead files on the web into R
For the download-button-averse of us
Every so often I’ll have a link to some file on hand and want to read it in R without going out of my way to browse the web page, find a download link, download it somewhere onto my computer, grab the path to it, and then finally read it into R.
-Over the years I’ve accumulated some tricks to get data into R “straight from a url”, even if the url does not point to the raw file contents itself. The method varies between data sources though, and I have a hard time keeping track of them in my head, so I thought I’d write some of these down for my own reference. This is not meant to be comprehensive though - keep in mind that I’m someone who primarily works with tabular data and use GitHub and OSF as data repositories.
+Over the years I’ve accumulated some tricks to get data into R “straight from a url”, even if the url does not point to the raw file contents itself. The method varies between data sources though, and I have a hard time keeping track of them in my head, so I thought I’d write some of these down for my own reference. This is not meant to be comprehensive though - keep in mind that I’m someone who primarily works with tabular data and interface with GitHub and OSF as data repositories.
GitHub has nice a point-and-click interface for browsing repositories and previewing files. For example, you can navigate to the dplyr::starwars
dataset from tidyverse/dplyr, at https://github.com/tidyverse/dplyr/blob/main/data-raw/starwars.csv:
It’s a similar idea with GitHub Gists (sometimes I like to store small datasets for demos as gists). For example, here’s a link to a simulated data for a Stroop experiment stroop.csv
: https://gist.github.com/yjunechoe/17b3787fb7aec108c19b33d71bc19bc6.
But that’s a full on webpage. The url which actually hosts the csv contents is https://gist.githubusercontent.com/yjunechoe/17b3787fb7aec108c19b33d71bc19bc6/raw/c643b9760126d92b8ac100860ac5b50ba492f316/stroop.csv, which you can again get to by clicking the Raw button at the top-right corner of the gist
+It’s a similar idea with GitHub Gists, where I sometimes like to store small toy datasets for use in demos. For example, here’s a link to a simulated data for a Stroop experiment stroop.csv
: https://gist.github.com/yjunechoe/17b3787fb7aec108c19b33d71bc19bc6.
But that’s again a full-on webpage. The url which actually hosts the csv contents is https://gist.githubusercontent.com/yjunechoe/17b3787fb7aec108c19b33d71bc19bc6/raw/c643b9760126d92b8ac100860ac5b50ba492f316/stroop.csv, which you can again get to by clicking the Raw button at the top-right corner of the gist
We now turn to the harder problem of accessing a file in a private GitHub repository. If you already have the GitHub webpage open and you’re signed in, you can follow the same step of copying the link that the Raw button redirects to.
Except this time, when you open the file at that url (assuming it can display in plain text), you’ll see the url come with a “token” attached at the end (I’ll show an example further down). This token is necessary to remotely access the data in a private repo. Once a token is generated, the file can be accessed using that token from anywhere, but note that it will expire at some point as GitHub refreshes tokens periodically (so treat them as if they’re for single use).
-For a more robust approach, you can use the GitHub Contents API. If you have your credentials set up in {gh}
(which you can check with gh::gh_whoami()
), you can request a token-tagged url to the private file using the syntax:
For a more robust approach, you can use the GitHub Contents API. If you have your credentials set up in {gh}
(which you can check with gh::gh_whoami()
), you can request a token-tagged url to the private file using the syntax:1
gh::gh("/repos/{user}/{repo}/contents/{path}")$download_url
@@ -1686,9 +1686,9 @@ [1] "https://raw.githubusercontent.com/yjunechoe/my-super-secret-repo/main/README.md?token=AMTCUR6BQGEERA..."
+ [1] "https://raw.githubusercontent.com/yjunechoe/my-super-secret-repo/main/README.md?token=AMTCUR2JPXCIX5..."
I can then use this url to read the private file:1
+I can then use this url to read the private file:2
gh::gh("/repos/yjunechoe/my-super-secret-repo/contents/README.md")$download_url |>
@@ -1718,7 +1718,7 @@ OSF
$ yield <int> 1545, 1440, 1440, 1520, 1580, 1540, 1555, 1490, 1560, 1495, 1595…
You might have already caught on to this, but the pattern is to simply point to osf.io/download/
instead of osf.io/
.
This method also works for view-only links to anonymized OSF projects as well. For example, this is an anonymized link to a csv file from one of my projects https://osf.io/tr8qm?view_only=998ad87d86cc4049af4ec6c96a91d9ad. Navigating to this link will show a web preview of the csv file contents, just like in the GitHub example with dplyr::starwars
.
This method also works for view-only links to anonymized OSF projects as well. For example, this is an anonymized link to a csv file from one of my projects https://osf.io/tr8qm?view_only=998ad87d86cc4049af4ec6c96a91d9ad. Navigating to this link will show a web preview of the csv file contents.
By inserting /download
into this url, we can read the csv file contents directly:
See also the {osfr}
package for a more principled interface to OSF.
Reading remote files aside, I think it’s severly under-rated how base R has a readClipboard()
function and a collection of read.*()
functions which can also read directly from a "clipboard"
connection.2
I sometimes do this for html/markdown summary tables that a website might display, or sometimes even for entire excel/googlesheets tables after doing a select-all + copy. For such relatively small chunks of data that you just want to quickly get into R, you can also lean on base R’s clipboard functionalities.
+Reading remote files aside, I think it’s severely underrated how base R has a readClipboard()
function and a collection of read.*()
functions which can also read directly from a "clipboard"
connection.3
I sometimes do this for html/markdown summary tables that a website might display, or sometimes even for entire excel/googlesheets tables after doing a select-all + copy. For such relatively small chunks of data that you just want to quickly get into R, you can lean on base R’s clipboard functionalities.
For example, given this markdown table:
cyl | mpg |
---|---|
4 | 26.66364 |
6 | 19.74286 |
8 | 15.10000 |
You can copy it and run the following code to get that data back as an R data frame:
+You can copy its contents and run the following code to get that data back as an R data frame:
read.delim("clipboard")
@@ -1779,7 +1779,7 @@ Aside: Can’t go wrong with a co
2 6 19.74286
3 8 15.10000
If you’re instead copying something flat like a list of numbers or strings, you can also use scan()
and specify the appropriate sep
to get that data back as a vector:3
If you’re instead copying something flat like a list of numbers or strings, you can also use scan()
and specify the appropriate sep
to get that data back as a vector:4
paste(1:10, collapse = ", ") |>
@@ -1816,10 +1816,22 @@ Streaming with {duckdb}
# A parquet file of tokens from a sample of child-directed speech
-file <- "https://raw.githubusercontent.com/yjunechoe/repetition_events/master/data/tokens_data/childID%3D1/part-7.parquet"
-
-
-In duckdb, the httpfs
extension allows PARQUET_SCAN
4 to read a remote parquet file.
+file <- "https://raw.githubusercontent.com/yjunechoe/repetition_events/master/data/tokens_data/childID%3D1/part-7.parquet"
+
+# For comparison, reading its contents with {arrow}
+arrow::read_parquet(file) |>
+ head(5)
+ # A tibble: 5 × 3
+ utterance_id gloss part_of_speech
+ <int> <chr> <chr>
+ 1 1 www ""
+ 2 2 bye "co"
+ 3 3 mhm "co"
+ 4 4 Mommy's "n:prop"
+ 5 4 here "adv"
+In duckdb, the httpfs
extension we loaded above allows PARQUET_SCAN
5 to read a remote parquet file.
query1 <- glue::glue_sql("
@@ -1885,7 +1897,7 @@ Streaming with {duckdb}
4 4 Mommy's n:prop 1
5 4 here adv 1
To do this more programmatically over all (parquet) files under /tokens_data
in the repository, we need to transition to using the GitHub Trees API. The idea is similar to using the Contents API but now we are requesting a list of all files using the following syntax:
To do this more programmatically over all parquet files under /tokens_data
in the repository, we need to transition to using the GitHub Trees API. The idea is similar to using the Contents API but now we are requesting a list of all files using the following syntax:
With recursive=true
, this returns all files in the repo. We can filter for just the parquet files we want with a little regex:
With recursive=true
, this returns all files in the repo. Then, we can filter for just the parquet files we want with a little regex:
parquet_files <- sapply(files, `[[`, "path") |>
- grep(x = _, pattern = ".*data/tokens_data/.*parquet$", value = TRUE)
+ grep(x = _, pattern = ".*/tokens_data/.*parquet$", value = TRUE)
length(parquet_files)
[1] 70
@@ -1931,7 +1943,7 @@ {duckdb}
Back on duckdb, we can use PARQUET_SCAN
to read multiple files by supplying a vector ['file1.parquet', 'file2.parquet', ...]
.5 This time, we also ask for a quick computation to count the number of distinct childID
s:
Back on duckdb, we can use PARQUET_SCAN
to read multiple files by supplying a vector ['file1.parquet', 'file2.parquet', ...]
.6 This time, we also ask for a quick computation to count the number of distinct childID
s:
query3 <- glue::glue_sql("
@@ -1955,7 +1967,7 @@ Streaming with {duckdb}
1 70
This returns 70
which matches the length of the parquet_files
vector listing the files that had been partitioned by childID.
For further analyses, we can CREATE TABLE
6 our data in our in-memory database con
:
For further analyses, we can CREATE TABLE
7 our data in our in-memory database con
:
query4 <- glue::glue_sql("
@@ -2046,21 +2058,22 @@ Streaming with {duckdb}
Other sources for data
In writing this blog post, I’m indebted to all the knowledgeable folks on Mastodon who suggested their own recommended tools and workflows for various kinds of remote data. Unfortunately, I’m not familiar enough with most of them enough to do them justice, but I still wanted to record the suggestions I got from there for posterity.
First, a post about reading remote files would not be complete without a mention of the wonderful {googlesheets4}
package for reading from Google Sheets. I debated whether I should include a larger discussion of {googlesheets4}
, and despite using it quite often myself I ultimately decided to omit it for the sake of space and because the package website is already very comprehensive. I would suggest starting from the Get Started vignette if you are new and interested.
-Second, along the lines of {osfr}
, there are other similar rOpensci packages for retrieving data from the kinds of data sources that may be of interest to academics, such as {deposits}
for zenodo and figshare, and {piggyback}
for GitHub release assets (Maëlle Salmon’s comment pointed me to the first two; I responded with some of my experiences). I was also reminded that {pins}
exists - I’m not familiar with it myself so I thought I wouldn’t write anything for it here BUT Isabella Velásquez came in clutch with a whole talk on dynamically loading up-to-date data with {pins} which is a great usecase demo of the unique strength of {pins}
.
+Second, along the lines of {osfr}
, there are other similar rOpensci packages for retrieving data from the kinds of data sources that may be of interest to academics, such as {deposits}
for zenodo and figshare, and {piggyback}
for GitHub release assets (Maëlle Salmon’s comment pointed me to the first two; I responded with some of my experiences). I was also reminded that {pins}
exists - I’m not familiar with it myself so I thought I wouldn’t write anything for it here BUT Isabella Velásquez came in clutch sharing a recent talk on dynamically loading up-to-date data with {pins} which is a great demo of the unique strengths of {pins}
.
Lastly, I inadvertently(?) started some discussion around remotely accessing spatial files. I don’t work with spatial data at all but I can totally imagine how the hassle of the traditional click-download-find-load workflow would be even more pronounced for spatial data which are presumably much larger in size and more difficult to preview. On this note, I’ll just link to Carl Boettiger’s comment about the fact that GDAL has a virtual file system that you can interface with from R packages wrapping this API (ex: {gdalraster}), and to Michael Sumner’s comment/gist + Chris Toney’s comment on the fact that you can even use this feature to stream non-spatial data!
Miscellaneous tips and tricks
I also have some random tricks that are more situational. Unfortunately, I can only recall like 20% of them at any given moment, so I’ll be updating this space as more come back to me:
-When reading remote .rda
or .RData
files with load()
, you need to wrap the link in url()
first (ref: stackoverflow).
+When reading remote .rda
or .RData
files with load()
, you may need to wrap the link in url()
first (ref: stackoverflow).
{vroom}
can remotely read gzipped files, without having to download.file()
and unzip()
first.
-{curl}
, of course, will always have the most comprehensive set of low-level tools you need to read any arbitrary data remotely. For example, using curl::curl_fetch_memory()
to read the dplyr::storms
data again from the GitHub raw contents link:
+{curl}
, of course, will always have the most comprehensive set of low-level tools you need to read any arbitrary data remotely. For example, using curl::curl_fetch_memory()
to read the dplyr::storms
data again from the GitHub raw contents link:
+
fetched <- curl::curl_fetch_memory(
- "https://raw.githubusercontent.com/tidyverse/dplyr/main/data-raw/starwars.csv"
- )
- read.csv(text = rawToChar(fetched$content)) |>
- dplyr::glimpse()
+ "https://raw.githubusercontent.com/tidyverse/dplyr/main/data-raw/starwars.csv"
+)
+read.csv(text = rawToChar(fetched$content)) |>
+ dplyr::glimpse()
Rows: 87
Columns: 14
@@ -2079,7 +2092,8 @@ Miscellaneous tips and tricks
$ vehicles <chr> "Snowspeeder, Imperial Speeder Bike", "", "", "", "Imperial…
$ starships <chr> "X-wing, Imperial shuttle", "", "", "TIE Advanced x1", "", …
-And even if you’re going the route of downloading the file first, curl::multi_download()
can offer big performance improvements over download.file()
.[^See an example implemented for {openalexR}
, an API package.] Many {curl}
functions also take a retry
parameter in some form which is cool too.
+
+Even if you’re going the route of downloading the file first, curl::multi_download()
can offer big performance improvements over download.file()
.8 Many {curl}
functions can also handle retries and stop/resumes which is cool too.
{httr2}
can capture a continuous data stream with httr2::req_perform_stream()
up to a set time or size.
sessionInfo()
@@ -2131,16 +2145,18 @@ sessionInfo()
-
+
-Note that the API will actually generate a new token every time you send a request (and again, these tokens will expire with time).↩︎
-The special value "clipboard"
works for most base-R read functions that take a file
or con
argument.↩︎
-Thanks @coolbutuseless for pointing me to textConnection()
!↩︎
-Or READ_PARQUET
- same thing.↩︎
-We can also get this formatting with a combination of shQuote()
and toString()
.↩︎
-Whereas CREATE TABLE
results in a physical copy of the data in memory, CREATE VIEW
will dynamically fetch the data from the source every time you query the table. If the data fits into memory (as in this case), I prefer CREATE
as queries will be much faster (though you pay up-front for the time copying the data). If the data is larger than memory, CREATE VIEW
will be your only option.↩︎
+Thanks @tanho for pointing me to this at the R4DS/DSLC slack.↩︎
+Note that the API will actually generate a new token every time you send a request (and again, these tokens will expire with time).↩︎
+The special value "clipboard"
works for most base-R read functions that take a file
or con
argument.↩︎
+Thanks @coolbutuseless for pointing me to textConnection()
!↩︎
+Or READ_PARQUET
- same thing.↩︎
+We can also get this formatting with a combination of shQuote()
and toString()
.↩︎
+Whereas CREATE TABLE
results in a physical copy of the data in memory, CREATE VIEW
will dynamically fetch the data from the source every time you query the table. If the data fits into memory (as in this case), I prefer CREATE
as queries will be much faster (though you pay up-front for the time copying the data). If the data is larger than memory, CREATE VIEW
will be your only option.↩︎
+See an example implemented for {openalexR}
, an API package.↩︎
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/anchor-4.2.2/anchor.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/anchor-4.2.2/anchor.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/anchor-4.2.2/anchor.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/anchor-4.2.2/anchor.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/bowser-1.9.3/bowser.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/bowser-1.9.3/bowser.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/bowser-1.9.3/bowser.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/bowser-1.9.3/bowser.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/distill-2.2.21/template.v2.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/distill-2.2.21/template.v2.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/distill-2.2.21/template.v2.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/distill-2.2.21/template.v2.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/header-attrs-2.27/header-attrs.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/header-attrs-2.27/header-attrs.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/header-attrs-2.27/header-attrs.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/header-attrs-2.27/header-attrs.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.map b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.map
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.map
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/jquery-3.6.0/jquery-3.6.0.min.map
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/popper-2.6.0/popper.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/popper-2.6.0/popper.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/popper-2.6.0/popper.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/popper-2.6.0/popper.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-bundle.umd.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-bundle.umd.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-bundle.umd.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-bundle.umd.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-light-border.css b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-light-border.css
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-light-border.css
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy-light-border.css
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.css b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.css
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.css
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.css
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.umd.min.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.umd.min.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.umd.min.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/tippy-6.2.7/tippy.umd.min.js
diff --git a/_posts/2024-09-01-fetch-files-web/fetch-files-web_files/webcomponents-2.0.0/webcomponents.js b/_posts/2024-09-22-fetch-files-web/fetch-files-web_files/webcomponents-2.0.0/webcomponents.js
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/fetch-files-web_files/webcomponents-2.0.0/webcomponents.js
rename to _posts/2024-09-22-fetch-files-web/fetch-files-web_files/webcomponents-2.0.0/webcomponents.js
diff --git a/_posts/2024-09-01-fetch-files-web/github-dplyr-starwars-csv.jpg b/_posts/2024-09-22-fetch-files-web/github-dplyr-starwars-csv.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/github-dplyr-starwars-csv.jpg
rename to _posts/2024-09-22-fetch-files-web/github-dplyr-starwars-csv.jpg
diff --git a/_posts/2024-09-01-fetch-files-web/github-dplyr-starwars-raw.jpg b/_posts/2024-09-22-fetch-files-web/github-dplyr-starwars-raw.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/github-dplyr-starwars-raw.jpg
rename to _posts/2024-09-22-fetch-files-web/github-dplyr-starwars-raw.jpg
diff --git a/_posts/2024-09-01-fetch-files-web/github-dplyr-starwars.jpg b/_posts/2024-09-22-fetch-files-web/github-dplyr-starwars.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/github-dplyr-starwars.jpg
rename to _posts/2024-09-22-fetch-files-web/github-dplyr-starwars.jpg
diff --git a/_posts/2024-09-01-fetch-files-web/github-gist-stroop.jpg b/_posts/2024-09-22-fetch-files-web/github-gist-stroop.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/github-gist-stroop.jpg
rename to _posts/2024-09-22-fetch-files-web/github-gist-stroop.jpg
diff --git a/_posts/2024-09-01-fetch-files-web/osf-MixedModels-dyestuff-download.jpg b/_posts/2024-09-22-fetch-files-web/osf-MixedModels-dyestuff-download.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/osf-MixedModels-dyestuff-download.jpg
rename to _posts/2024-09-22-fetch-files-web/osf-MixedModels-dyestuff-download.jpg
diff --git a/_posts/2024-09-01-fetch-files-web/osf-MixedModels-dyestuff.jpg b/_posts/2024-09-22-fetch-files-web/osf-MixedModels-dyestuff.jpg
similarity index 100%
rename from _posts/2024-09-01-fetch-files-web/osf-MixedModels-dyestuff.jpg
rename to _posts/2024-09-22-fetch-files-web/osf-MixedModels-dyestuff.jpg
diff --git a/docs/blog.html b/docs/blog.html
index e61d3dc8..3daf1b4e 100644
--- a/docs/blog.html
+++ b/docs/blog.html
@@ -2784,6 +2784,22 @@ ${suggestion.title}