How It's Made
Published · Updated
This is how I made the Museum of Websites (MOW) in 2024, and the lessons I learned along the way (updated in 2025 and 2026). #BuildInPublic
Tech Stack
When I started this project in early 2025, I had human devs and AI recommend that I use React and Next.js because that is where all the jobs are these days. However, I chose to avoid Next.js because it seems designed to favor Vercel’s hosting ecosystem and to lock you into it. Though I do have to admit that I love hosting with Vercel and have used them for this project. 🤭
Also, I dislike how React files lack support for style tags within components and how there is no built-in CSS scoping like with Vue, Svelte, or Astro.
Astro
I chose Astro because…
✅ The content for this website is mostly static, and Astro allows you to start static-only and add dynamic functionality if/when needed. Jason from CodeTV has the best explanation of Astro that I’ve seen:
✅ I love the flexibility. You can make some pages pre-rendered, while other pages are server-side rendered. And even the pages that are pre-rendered can have live, dynamic content thanks to dynamic islands / partial hydration.
✅ The dynamic content that I did have planned (user views, likes, and comments) could theoretically be added as dynamic islands / partial hydration, and I was curious to see how far I could push this.
✅ I love the .astro file structure. It’s basically the same as a .html file with embedded <style> and <script> sections, and this is how I originally learned webdev as a 90s kid. The only difference is that a .astro file includes a frontmatter section where you can write JavaScript that is meant to execute only during build time (for pre-rendered pages) or on the server (for server-side rendered pages).
✅ I love that it ships with no JavaScript by default. This allows you to make the site very lean.
✅ I love that you can choose to use client-side components from React, Vue, or Svelte, or go entirely vanilla JS, or do a combination of all 4 (though I would not recommend this). This allowed me to dabble with these other frameworks while working in Astro and realize that I hate the structure of .jsx files, that Svelte has the most similar structure to Astro, and that I can implement the component using vanilla JS and an endpoint running server-side on Astro.
✅ I heard a ton of good things about Astro from comments and blogs while catching up on the JavaScript framework scene in 2024 and 2025. There’s so much love and passion and excitement in the Astro community that is unmatched by others.
✅ I love that Astro allows you to use Markdown files as a flat-file content management system (CMS), so you can avoid having to set up and run a database for the core content. Another benefit is that you use GitHub as free version control and backup.
There are downsides to a flat-file approach, though, the biggest being ‘schema duplication’. Each website featured on MOW has a folder in the /src/content/websites/ directory, and within each website’s folder there is an index.md file along with image and video files for screenshots and screen recordings. The frontmatter in each index.md file contains all the fields that are used in filters and page templates. This makes it quick and easy to edit the values; however, the downside is that if you need to add/edit/remove any field name, this has to be repeated for every index.md file in that collection. You’ll have to do these updates manually file by file or write a custom migration script to automate the process. In contrast, in a database, you can just rename the column once, and it applies to all the entries.
When I started building MOW, I had to edit the schema and index.md files frequently while ironing out the design. So to minimize the update costs, I started with only a few website entries and only added more once I was confident I had finalized a schema.
In hindsight, it might have been easier to use a database (or a Database As A Service like Supabase or Baserow) for most of the website fields instead of using the frontmatter in markdown files.
UPDATE: With Codex, this is no longer an issue, as I can get AI to quickly mass-update the schema.
✅ Lastly, I love that Astro allows you to use a regular database if you want to, and that’s what I did for the dynamic content.
Supabase
For dynamic features like user authentication, user likes, user comments, and page view counters, a database is needed.
For this project, I opted for a Database as a Service (DBaaS) so that I can focus on launching quickly and on learning Astro.
The two most popular options are Supabase (PostgreSQL) and Google’s Firebase (NoSQL). Appwrite, Neon, Sentry, Turso, and Xata are also mentioned by Astro. Others have brought up PocketBase (SQLite) as an alternative solution that you can self-host.
I chose Supabase (PostgreSQL) because…
- I already had SQL experience.
- Supabase has a decent free tier.
- Supabase includes auth services.
- Supabase is open-source.
- Supabase has a good reputation among users.
- Supabase has a large amount of tutorial content and documentation, including by Astro itself.
- Supabase has automatic security warnings.
I avoided Firebase (NoSQL) because…
- I have fatigue with Google’s declining quality, horrible support, and seeming disdain for its own users. For example, I’ve had a Google Business listing suddenly get taken down with no explanation, and support was only able to tell me to read the Terms of Service. I read the terms of service line by line, couldn’t find any violation, and support would continue to refuse to clarify what the issue was. Experiences like this have killed my trust in anything Google. And that’s before even considering the risk of Google killing off its own product.
- Firebase allows for easy misconfiguration of security rules with zero warnings.
- Firebase uses a Domain Specific Language (DSL) for security rules, database rules, and storage rules. Unlike SQL, this DSL is not useful or transferable anywhere outside of Firebase.
- Firebase is not open-source.
I avoided PocketBase (SQLite) because…
- PocketBase does not support Row Level Security (RLS).
- PocketBase is not even on v1 yet.
- The PocketBase team has stopped being active on Twitter / X.com.
Things I learned using Supabase:
-
Make sure to pay attention to the security warnings on the dashboard. There was a viral post on X.com from a vibe coder who had their Supabase hacked. It turned out he forgot to limit his views and functions to security invoker (vs security definer), and this is something I’ve seen Supabase warn directly about in the dashboard and via email.
-
For any table tracking private user activity, make sure to enable Row Level Security (RLS) and then set policies for it. If you have RLS enabled on a table, but haven’t set RLS policies yet, then you won’t be able to read or write anything to the table.
-
Know when to ignore Supabase warnings. For example, if you want to make aggregate data available to everyone from a table that tracks user activity and has RLS policies, you will have to create a view that runs as security definer. In Museum of Websites, this situation came up when I wanted to make each user’s likes private, but the total like count to be public.
-
By default, for views, Supabase grants all privileges to every user for some odd reason. So make sure to revoke everything from anon and authenticated users, and then grant them SELECT privileges.
REVOKE ALL ON public.aggregate_likes FROM anon, authenticated; GRANT SELECT ON public.aggregate_likes TO anon, authenticated;
-
The alternative to running a view as security definer would be to have a “materialized view”. But when trying to set up materialized views and have them immediately update with each change to the underlying table, I found them to be a pain to work with for my use case. They are mostly suitable for data that doesn’t change often and/or that is ok to be a bit stale.
-
The default “public” schema doesn’t mean that every table within the schema is public.
-
When using Supabase with Astro, you can run the Supabase client on server-side rendered (SSR) pages or within pre-rendered pages using client-side components like Svelte, Vue, or React. However, when using Supabase with client-side components, you will have to expose the ANON key to the public. This requires taking extra security precautions. To avoid that, I chose to run Supabase only on SSR API endpoints in Astro and then access those endpoints from pre-rendered pages using vanilla JavaScript.
-
Supabase’s AI Assistant is not very useful (as of June 2025) and will sometimes even give you invalid code. I found ChatGPT 4o to be more helpful and accurate.
AI tools (Cursor → ChatGPT → Claude → Codex)
When I first started building this site, I tried a mix of ChatGPT and Cursor.
While I sometimes enjoyed the autocomplete that Cursor offered, I had mixed results when using it to create new features from scratch or to fix issues with existing ones.
I found ChatGPT (v3 and v4) to be much more reliable. However, I was using it like a caveman: copying and pasting code from the chat whenever I got stuck while coding manually.
ChatGPT also somewhat frequently gave me bad code suggestions or completely misunderstood the context and requirements. I’ve found my previous web dev, computer science, and product management backgrounds to be critical for working successfully with AI and being able to challenge it when necessary. The more you understand the fundamentals of your work, the more productive AI can make you.
Then I tried Claude, which was game-changing. I gave it access to my repo and directed it to add features or fix things simply by chatting with it. This is when I started returning to my role as a Product Manager rather than a coder.
My main complaint with Claude was that the way it talked felt much more artificial, robotic, and verbose than ChatGPT. Even after trying to change that with instructions, it never felt as natural as ChatGPT to me.
Luckily, ChatGPT made its own version of Claude called Codex, and it ended up becoming my preferred tool for building from 2025 onwards. My only complaint with Codex is that its design skills aren’t as good out of the box as Claude’s. So I’ll still occasionally use Claude for designing and styling a website, then use Codex to build out the functionality and features.
Overall, both Claude and Codex are now able to provide me with high-quality code that requires minimal supervision.
However, it’s important to note that I make sure all my product requirements are documented. I also have Claude and Codex automatically document the things I ask them to do or change, along with the context and reasoning behind it. I’ve found that this helps a lot with code quality and preventing regressions and bugs.
Github
This one is obvious for anyone with webdev experience, but seems to be forgotten by new devs and vibecovers, so it’s worth bringing it up.
GitHub is a way to store your code and track changes made to it. It’s pretty much unavoidable when working in teams, but also super helpful when working solo. Whenever I add a new feature or fix a bug, I make sure to save it as a “commit.” Think of it like making checkpoints in a video game.
This is extra important if you’re letting AI make changes to your code.
Media
Every website featured on the Museum of Websites needs a screenshot and a video, and this is something I haven’t been able to fully automate yet. For example, in 2026, I tried Grok Bot; however, its screen was limited to 1280×800 at 100 DPI (i.e., non-Retina), which was too small for the dimensions I need.
Here are the processes I follow to make sure the screenshots and videos look consistent.
Screenshots
For screenshots, I was pleasantly surprised to learn that the built-in DevTools in Chrome browser include a tool for this. The tool even has a full-size option that automatically captures the entire length of the site without the scrollbar included.
This is something that even premium, paid tools like CleanShotX aren’t able to do. For example, the “scrolling capture” in CleanShotX leaves behind scrollbar artifacts that need to be cleaned up in another app like Photoshop.
Instructions for screenshots in Chrome:
- Open Inspect mode in Chrome’s DevTools (Command + Option + C in MacOS),
- Set the viewport size to 1600px x 1000px for an aspect ratio of 16:10,
- Open the Command Menu (Command + Shift + P in MacOS) and type in “screenshot”,
- Select “Capture full size screenshot” to capture the entire page,
or “Capture screenshot” to capture only the visible part of the page.
Note 1: Depending on how a website has been built and how long it is, the “full” screenshot option may not capture everything properly. Sometimes I have to separately screenshot the top and bottom part of a side for our previews.
Note 2: I use an iMac 24” with a Retina display, which means that a viewport of 1600x1000 actually gives an output of 3200x2000. I then get Astro to downscale the image depending on what device and viewport the user is on.
Video
For videos, the process starts the same as for screenshots, but then requires an additional tool like CleanShotX.
In Chrome browser…
- Open Inspect mode in Chrome’s DevTools (Command + Option + C in MacOS),
- Check that the viewport size is still 1600px x 1000px (as explained earlier).
On CleanShotX…
- In Settings > Recording > General,
- turn OFF “Scale Retina videos to 1x”
- turn ON “‘Do Not Disturb’ while recording”
- turn OFF “Show cursor”
- turn OFF “Highlight clicks”
- turn ON “Remember last selection”
- In Settings > Recording > Video,
- set “Video FPS” to “60”
(you can lower it to 30 if you want to save space, but I find 60 to be the best balance between quality and file size).
- set “Video FPS” to “60”
- In Settings > Shortcuts,
- for “Screen Recording” > “Record Screen / Stop Recording”, set Control + Q.
(This will allow you to quickly stop recording with your left hand while keeping your right hand on the trackpad or arrow keys for scrolling).
- for “Screen Recording” > “Record Screen / Stop Recording”, set Control + Q.
Then I press Control + Q to start recording, scroll down using the down arrow key (unless the site has some particularly cool functionality that is related to mouse tracking), and then press Control + Q to stop the recording.
CleanShotX saves the video output in .mp4 format, which is ideal for the web.
But when I tried to reduce the video from 3200x2000 to 1600x1000, I ran into some issues:
-
** QuickTime ** wouldn’t allow me to set custom dimensions. The closest was 1080p. Also, it only exports as .mov, which can have playback issues on the web on non-Mac devices.
-
VLC Player would only give me files in .m4v format even when selecting MP4, and I ran into playback issues with this format on my website when used within modals.
So then I found ffmpeg and it worked beautifully! Note, though, that it requires you to be comfortable using Terminal/command line. Once I had it installed, I’d then run this command for the conversion:
ffmpeg -i input.mp4 -vf "scale=1600:-2" -c:a copy output.mp4
Hosting
I tried both Vercel and Netlify, and Vercel was by far the better of the two.
Vercel has better transparency, details, and notifications when there are issues with a deployment. Whereas on Netlify, my early deployments would fail without any warning, and when I would go check manually, it was difficult to troubleshoot what went wrong.
Furthermore, on Netlify’s free Hobby plan, even after a successful build and deployment, I ran into the following issues:
- Video files were being excluded.
- Likes and views data from Supabase weren’t loading, but auth via Supabase did work.
So using Netlify just kept giving me all sorts of random headaches that weren’t happening with other platforms.
Video Storage
As the Museum of Website grew into hundreds and thousands of site entries, including videos in the build itself was no longer feasible. I had to store the videos separately using Cloudflare’s R2 object storage.



