Skip to content

Commit ec0917a

Browse files
committed
album exchange blog post and SEO improvements
1 parent baf9e6d commit ec0917a

File tree

5 files changed

+249
-30
lines changed

5 files changed

+249
-30
lines changed

_config.yml

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,20 @@
3535
# - jekyll-feed
3636

3737
# Site settings
38-
title: bhav
38+
title: whybhav
3939
email: me@whybhav.in
40-
description: personal portfolio website and blog
40+
description: >-
41+
Personal portfolio and blog of Vaibhav (bhav) - a 24 y/o graduate student who loves making cool stuff, photography, and writing about technology and life.
4142
baseurl: "" # Leave empty for username.github.io sites
4243
url: "https://whybhav.in"
4344
github_username: codebhav
4445

46+
# SEO and social
47+
author: Vaibhav Achuthananda
48+
tagline: "Personal portfolio and blog"
49+
4550
# Build settings
4651
markdown: kramdown
47-
# theme: minima # We'll customize this minimal theme
4852
plugins:
4953
- jekyll-feed
5054
- jekyll-seo-tag
@@ -63,6 +67,7 @@ defaults:
6367
values:
6468
layout: "post"
6569
comments: true
70+
author: Vaibhav Achuthananda
6671
- scope:
6772
path: ""
6873
type: "photos"
@@ -73,6 +78,14 @@ defaults:
7378
type: "pages"
7479
values:
7580
layout: "page"
81+
author: Vaibhav Achuthananda
82+
83+
# SEO defaults
84+
defaults:
85+
- scope:
86+
path: ""
87+
values:
88+
image: /social-preview.png
7689

7790
compress_html:
7891
clippings: all
@@ -81,14 +94,7 @@ compress_html:
8194
blanklines: false
8295
profile: false
8396

84-
# Exclude from processing.
85-
# The following items will not be processed, by default.
86-
# Any item listed under the `exclude:` key here will be automatically added to
87-
# the internal "default list".
88-
#
89-
# Excluded items can be processed by explicitly listing the directories or
90-
# their entries' file path in the `include:` list.
91-
#
97+
# Exclude from processing
9298
exclude:
9399
- Gemfile
94100
- Gemfile.lock
@@ -98,4 +104,4 @@ exclude:
98104
- .jekyll-cache/
99105
- gemfiles/
100106
- Gemfile.lock
101-
- node_modules/
107+
- node_modules/
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,52 @@
11
---
22
layout: post
3-
title: "a brand new old website"
3+
title: "A Brand New Old Website"
44
date: 2025-05-17 17:19:00 -0400
55
categories: general
66
tags: portfolio
77
---
88

9-
welcome to yet another iteration of my website.
9+
Welcome to yet another iteration of my website.
1010

1111
<!-- more -->
1212

13-
i built the first version of my portfolio, even before i owned this domain, shortly after my first ever web programming class during my undergrad, using plain html, css, and js.
13+
I built the first version of my portfolio, even before I owned this domain, shortly after my first web programming class during my undergrad, using plain HTML, CSS, and JS.
1414

1515
<figure>
16-
<img src="/assets/images/blog/website-v1.png" alt="first version of vaibhav's website">
17-
<figcaption>portfolio v1</figcaption>
16+
<img src="/assets/images/blog/website-v1.png" alt="first version of vaibhav's website">
17+
<figcaption>portfolio v1</figcaption>
1818
</figure>
1919

20-
after about a year, i built the second version—a single-page threejs application with a cool 3d render (gltf) of a pc setup. this is, by far, the flashiest project i’ve ever worked on. i learned so much when i was working on it, like the fact that i do not enjoy working with 3d files.
20+
After about a year, I built the second version—a single-page ThreeJS application with a cool 3D render (GLTF) of a PC setup. This is, by far, the flashiest project I've ever worked on. I learned so much when I was working on it, like the fact that I do not enjoy working with 3D files.
2121

2222
<figure>
23-
<img src="/assets/images/blog/website-v2.png" alt="second version of vaibhav's website">
24-
<figcaption>portfolio v2</figcaption>
23+
<img src="/assets/images/blog/website-v2.png" alt="second version of vaibhav's website">
24+
<figcaption>portfolio v2</figcaption>
2525
</figure>
2626

27-
like clockwork, i got bored after a year and built a new one overnight that was much simpler using reactjs. this time, it even had a blog section—but i never published anything, so did it really have a blog section?
27+
Like clockwork, I got bored after a year and built a new one overnight that was much simpler using ReactJS. This time, it even had a blog section—but I never published anything, so did it really have a blog section?
2828

29-
it was a really shitty website, probably the worst one of the three so far. but the homepage was a clean bento-grid layout, so i let it hang around for a while, thinking i’d fix all its flaws during christmas.
29+
It was a really shitty website, probably the worst one of the three so far. But the homepage was a clean bento-grid layout, so I let it hang around for a while, thinking I'd fix all its flaws during Christmas.
3030

3131
<figure>
32-
<img src="/assets/images/blog/website-v3.png" alt="third version of vaibhav's website">
33-
<figcaption>portfolio v3</figcaption>
32+
<img src="/assets/images/blog/website-v3.png" alt="third version of vaibhav's website">
33+
<figcaption>portfolio v3</figcaption>
3434
</figure>
3535

36-
i tried about four or five variations of this new site, even building them using different frameworks. my use case was pretty simple: i needed a blog site to yap, with a place for people to leave comments if they wish to do so and to post the photos i’ve taken.
36+
I tried about four or five variations of this new site, even building them using different frameworks. My use case was pretty simple: I needed a blog site to yap, with a place for people to leave comments if they wish to do so and to post the photos I've taken.
3737

38-
at last, i have settled on an extremely simple yet elegant solution: jekyll + github pages. this site is almost entirely markdown-based, and dare i say, _i fw it heavy_. i really like how clean and simple it looks. i intend to use this space to write about projects i'm working on and also to talk about anything and everything i find interesting.
38+
At last, I have settled on an extremely simple yet elegant solution: Jekyll + GitHub pages. This site is almost entirely markdown-based, and dare I say, _i fw it heavy_. I really like how clean and simple it looks. I intend to use this space to write about projects I'm working on and also to talk about anything and everything I find interesting.
3939

40-
here’s the link to my [rss feed](https://whybhav.in/feed.xml)
40+
Here's the link to my [RSS feed](https://whybhav.in/feed.xml)
4141

42-
see you around!
42+
See you around!
4343

4444
<br>
4545

4646
---
4747

4848
<br>
4949

50-
ps: thanks to everyone who continues to make cool shit, and thanks to the people who've inspired me to build this site: [den](https://den.dev/), [neal](https://neal.fun/), [nolen](https://eieio.games/), [tru](https://mewtru.com/), [jan](https://netmeister.org/), [manuel](https://manuelmoreale.com/), [ezri](https://ezrizhu.com/), [melody](https://melody.codes/), and many, many more.
50+
PS: thanks to everyone who continues to make cool shit, and thanks to the people who've inspired me to build this site: [den](https://den.dev/), [neal](https://neal.fun/), [nolen](https://eieio.games/), [tru](https://mewtru.com/), [jan](https://netmeister.org/), [manuel](https://manuelmoreale.com/), [ezri](https://ezrizhu.com/), [melody](https://melody.codes/), and many, many more.
5151

52-
pps: cute cat that follows the cursor: [github.com/adryd325/oneko.js](https://github.com/adryd325/oneko.js/)
52+
PPS: cute cat that follows the cursor: [github.com/adryd325/oneko.js](https://github.com/adryd325/oneko.js/)
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
---
2+
layout: post
3+
title: "The Album Exchange Story"
4+
date: 2025-06-06 16:32:00 -0400
5+
categories: general
6+
tags: album-exchange music spotify
7+
---
8+
9+
During the height of Covid, a bunch of brave people gathered around a table and said, "Enough is enough!" and decided to take matters into their own hands. So they made a Discord channel to pair up random people and recommend music, and this was called Album Exchange.
10+
11+
That no longer exists, and Spotify's recommendation algorithm—pardon my language—sucks ass, which is why I have decided to be brave and build a website to get album recommendations.
12+
13+
<figure>
14+
<img src="/assets/images/blog/album-exchange.png" alt="Social preview for Album Exchange">
15+
<figcaption>Album Exchange</figcaption>
16+
</figure>
17+
18+
## The Core Idea
19+
20+
The concept is intentionally simple:
21+
22+
- I feature a weekly album pick on the homepage
23+
- You can submit one album recommendation per week
24+
- Your submission automatically becomes a playlist on my Spotify account
25+
- Everyone can browse the gallery of all submitted albums
26+
- The site shows what I'm currently listening to (when I am)
27+
28+
## Technical Challenges
29+
30+
### The Spotify API
31+
32+
I have never used Spotify's API before, so I had to learn to do a few things like:
33+
34+
1. Fetch album details for display
35+
2. Create playlists automatically from submissions
36+
3. Show my current listening status
37+
4. Handle rate limits gracefully
38+
39+
The rate limit challenge was enjoyable. Spotify is generous with its limits, but you can hit walls quickly when fetching album art and details for every submission. I ended up implementing a caching strategy with multiple fallbacks:
40+
41+
```javascript
42+
// cache album details for 24h
43+
const albumDetailsCache = new Map();
44+
const ALBUM_CACHE_DURATION = 24 * 60 * 60 * 1000;
45+
46+
// retry logic with exponential backoff
47+
async function fetchWithRetry(fetchFn, maxRetries = MAX_RETRIES) {
48+
let retryDelay = INITIAL_RETRY_DELAY;
49+
50+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
51+
try {
52+
return await fetchFn();
53+
} catch (error) {
54+
if (error.status === 429) {
55+
const waitTime = error.retryAfter
56+
? error.retryAfter * 1000
57+
: retryDelay;
58+
await new Promise((resolve) => setTimeout(resolve, waitTime));
59+
retryDelay *= 2;
60+
}
61+
}
62+
}
63+
}
64+
```
65+
66+
### Preventing Spam
67+
68+
I hate when websites make me sign up. I just want people to be able to submit recommendations, but I also didn't want someone to flood the site with a 47-album playlist of "souljaboytellem.com." So, I needed a rate-limiting system without asking the users to sign up.
69+
70+
I went with a combination of IP address and browser fingerprinting:
71+
72+
```javascript
73+
function generateSubmissionId(ip, fingerprint) {
74+
return crypto
75+
.createHash("sha256")
76+
.update(ip + (fingerprint || "") + (process.env.IP_SALT || ""))
77+
.digest("hex");
78+
}
79+
```
80+
81+
This creates a unique but anonymized identifier for each user. The salt means I can't reverse-engineer the original data, and the one-way hash means the system is privacy-friendly while preventing abuse.
82+
83+
The rate limit resets every Monday at midnight UTC, which gives the site a nice weekly rhythm.
84+
85+
### The Playlist Creation Magic
86+
87+
This part is my favorite. When someone submits an album, I don't just store it in a database—I create a Spotify playlist on my account with all the tracks from that album.
88+
89+
```javascript
90+
export async function createAlbumPlaylist(albumId, nickname, albumName) {
91+
const api = await getSpotifyApi();
92+
93+
// get my user profile
94+
const me = await api.currentUser.profile();
95+
96+
// create playlist with format "nickname-album title"
97+
const playlistName = `${nickname}-${albumName}`.substring(0, 100);
98+
const playlist = await api.playlists.createPlaylist(me.id, {
99+
name: playlistName,
100+
description: `album recommendation from ${nickname} via bhav.fun`,
101+
public: false,
102+
});
103+
104+
// get all tracks from the album
105+
const albumTracks = await api.albums.tracks(albumId);
106+
107+
// add them to the playlist
108+
if (albumTracks.items.length > 0) {
109+
const trackUris = albumTracks.items.map((track) => track.uri);
110+
await api.playlists.addItemsToPlaylist(playlist.id, trackUris);
111+
}
112+
113+
return playlist;
114+
}
115+
```
116+
117+
Which means every recommendation becomes an immediately playable playlist. I realize I have let the internet control my Spotify; there is a non-zero chance I wake up to my Spotify having a playlist called "Penis-Punisher." But please don't do that. I beg.
118+
119+
## Architecture: Next.js + Firebase + Prayers
120+
121+
The stack is pretty straightforward:
122+
123+
- **Next.js** for the frontend and API routes
124+
- **Firebase Firestore** for storing submissions
125+
- **Spotify Web API** for everything music-related
126+
- **Vercel** for hosting
127+
128+
I organized it around clear separation of concerns:
129+
130+
```
131+
├── app/ # Next.js pages and API routes
132+
├── components/ # React components (all the cozy UI bits)
133+
├── lib/ # Services and utilities
134+
│ ├── spotify-service.js # All Spotify API interactions
135+
│ ├── firebase-service.js # Database operations
136+
│ ├── fingerprint-service.js # Rate limiting
137+
│ └── url-utils.js # Input validation and cleaning
138+
```
139+
140+
The API design is RESTful but pragmatic. For example, the submission endpoint does the following:
141+
142+
1. Validate the rate limit
143+
2. Sanitizes inputs
144+
3. Validates Spotify URLs
145+
4. Fetches album details
146+
5. Create the playlist
147+
6. Stores everything in the database
148+
149+
It's not the most "pure" API design, but it works well for this use case where each submission is a complex multi-step operation.
150+
151+
## Some Interesting Problems I Solved
152+
153+
### Dynamic Album Cards
154+
155+
I wanted album cards that could dynamically load their artwork and details from just a Spotify URL. This led to an interesting caching strategy where I cache both in-browser (localStorage) and server-side, with intelligent fallbacks when things go wrong.
156+
157+
### Responsive Image Handling
158+
159+
Album artwork comes in various sizes from Spotify, and I needed it to look good across devices. Next.js's Image component helped a lot, but I still had to handle edge cases like missing artwork:
160+
161+
```jsx
162+
<Image
163+
src={albumImage || "/images/album-placeholder.jpg"}
164+
alt={`Album cover: ${albumName} by ${artistName}`}
165+
width={300}
166+
height={300}
167+
quality={85}
168+
placeholder="blur"
169+
blurDataURL="data:image/svg+xml,..."
170+
onError={onImageError}
171+
/>
172+
```
173+
174+
### The "Now Playing" Feature
175+
176+
I quickly realized this was slightly more nuanced than expected because Spotify's "currently playing" endpoint doesn't always have data. So I fall back to "recently played" and show that instead, with appropriate labeling. It makes the site feel more alive and personal.
177+
178+
## The Unexpected Joy of Building Something Small
179+
180+
<figure>
181+
<img src="/assets/images/blog/the-zen-of-python.png" alt="Zen of Python by Tim Peters">
182+
<figcaption>The Zen of Python</figcaption>
183+
</figure>
184+
185+
One of the most rewarding aspects of this project was its scope. I built and shipped the MVP in two days, and it immediately felt proper and complete. There's something deeply satisfying about creating a tool that solves exactly one problem well.
186+
187+
It also forced me to make decisions quickly. Should I build user accounts? Should I add social features? Should I integrate with other music services? For now, the answer to all of these is "no," and that simplicity is part of what makes the site charming.
188+
189+
## Some Technical Details You Might Care About
190+
191+
**Security:** All user inputs are sanitized, URLs are validated to ensure they're actually Spotify links, and the rate-limiting system is designed to be privacy-friendly.
192+
193+
**Performance:** Album details are cached aggressively, images are optimized with Next.js's Image component, and the Spotify API calls include retry logic and error handling.
194+
195+
**Accessibility:** I tried to follow best practices, such as semantic HTML, proper focus management, alt text for images, and keyboard navigation support.
196+
197+
**SEO:** The site has proper meta tags, a sitemap, and structured data to help with discovery.
198+
199+
## What's Next
200+
201+
As sad as it is, my hyper fixation on this site has worn off, but I'm sure I will revisit this, and when I do, I'd like to make:
202+
203+
- Better error handling for edge cases
204+
- More granular caching strategies
205+
- Maybe a Grand Spotify playlist that's automatically updated with all submissions
206+
207+
But I'm pretty happy with it as it is. Sometimes, the best software is the kind that does exactly what it says it will do without trying to be everything to everyone.
208+
209+
## Try It Out
210+
211+
You can check out [Album Exchange](https://bhav.fun/) yourself. Please submit an album you love, browse what others have recommended, or look around the source code on [GitHub](https://github.com/codebhav/album-exchange).
212+
213+
And if you build something similar or have ideas for improvements, [I'd love to hear about it](https://whybhav.in/contact/). The best part of sharing projects like this is the conversations they start.

assets/images/blog/album-exchange.png

169 KB
Loading
843 KB
Loading

0 commit comments

Comments
 (0)