Back
Roast
SEO from a Newbie for Beginners
So, youāve got a bootstrap project, an awesome product, but few customers. My time has come to dive into SEO.
Running a bootstrap or indie project really sharpens a lot of skills and teaches you to focus more on business (money). Plus, you realize that marketers arenāt just sitting aroundātheyāve got a tough job too!
Running a bootstrap or indie project really sharpens a lot of skills and teaches you to focus more on business (money). Plus, you realize that marketers arenāt just sitting aroundātheyāve got a tough job too!
What follows is a pretty basic rundown of things Iāve learned and done over the last few days (or weeks).
First Things First: The Domain
They say .com is slightly better, but overall, it doesnāt really matter. If your domain isnāt brand new, thatās even betterādomain reputation and backlinks matter! This is also why buying underperforming but promising projects (including domains) can pay off if you know what youāre doing.
We snagged aso.dev for a magical $98 (or close to that).
aso.dev.png
518 KB
Next, you build the site.
Since SEO is a priority, server-side rendering (SSR) is a must. Thereās a lot of talk about Google being able to parse JavaScript, but thatās mostly nonsense. Sometimes it can, but it generally hates doing so, so donāt risk it. You could use a heavy SSR setup for React or Angular, but we went with astro.buildāsuper fast, simple, and elegant. They have plenty of free and complex themes, but we settled on their Starlight theme after tweaking the home page a bit (you need to copy the Hero.astro component and override Head.astro).
Starlight.png 171 KB
Iām loving it so far. Weāre even reworking the site for our player, meows.app, because dynamic rendering based on API responses works perfectly. We hired a junior dev to rewrite it from scratch on the same tech stack for cheapāgreat practice for him and a faster, simpler site for us. Angular 14 just isnāt cutting it anymore, even with Universal rendering (feels like ages ago)ācheck out our WIP example with Astro.
Next, you build the site.
Since SEO is a priority, server-side rendering (SSR) is a must. Thereās a lot of talk about Google being able to parse JavaScript, but thatās mostly nonsense. Sometimes it can, but it generally hates doing so, so donāt risk it. You could use a heavy SSR setup for React or Angular, but we went with astro.buildāsuper fast, simple, and elegant. They have plenty of free and complex themes, but we settled on their Starlight theme after tweaking the home page a bit (you need to copy the Hero.astro component and override Head.astro).
Starlight.png 171 KB
Iām loving it so far. Weāre even reworking the site for our player, meows.app, because dynamic rendering based on API responses works perfectly. We hired a junior dev to rewrite it from scratch on the same tech stack for cheapāgreat practice for him and a faster, simpler site for us. Angular 14 just isnāt cutting it anymore, even with Universal rendering (feels like ages ago)ācheck out our WIP example with Astro.
Itās looking pretty good. Previously, we used tinypng for compressing images (manually or through API + GPT script), but now weāre sticking to the built-in image optimization tools in astro.build.
seo_perf_chrome.png 87.8 KB
Google Search Console
Next up: Google Search Console. Add your site to track indexing, spot errors, and unlock achievements (which you can flex on Twitter).
achivements.png 56.3 KB
Google Search Console
search_console.png 96.9 KB
There are plenty of tools out there, but for now, Iām sticking with a couple of free ones.
Ahrefs
Ahrefs is great because you get 10k site queries and a detailed analysis of your pagesā issues. You canāt fix everything, but reducing errors helps a lot.
This tool provided most of the insights Iāve used to improve my site.
Fixes and Improvements
Optimizing Page Titles and Descriptions
Titles should be 50-60 characters, and descriptions 110-160. In the meta structure of my .md files, I added them like this:
seo: seo_title: "All-in-One ASO Solution for iOS Developers, marketing" seo_description: "ASO.dev is ultimate tool for App Store Optimization (ASO) with App Store Connect integration.Manage,optimize,grow your apps effortlessly with powerful features"
Then I asked GPT to write a Bash script to check these files. After about 30 tries, it finally worked (still faster than if I did it by hand):
# Function to check the length of seo_title and seo_description check_seo_params() { local file=$1 local in_seo_block=false local seo_title="" local seo_description="" while IFS= read -r line do # Look for the start of the seo block if [[ "$line" =~ ^seo: ]]; then in_seo_block=true fi # If inside the seo block, search for seo_title and seo_description if [[ "$in_seo_block" = true ]]; then # Search for seo_title if [[ "$line" =~ seo_title:[[:space:]]*\"(.*)\" ]]; then seo_title="${BASH_REMATCH[1]}" fi # Search for seo_description if [[ "$line" =~ seo_description:[[:space:]]*\"(.*)\" ]]; then seo_description="${BASH_REMATCH[1]}" fi fi # If the seo block ends (new block or end of file), stop reading if [[ "$in_seo_block" = true && "$line" =~ ^[^[:space:]] && ! "$line" =~ ^seo ]]; then in_seo_block=false fi done < "$file" local have_errors=false # Check if seo_title is present and valid if [[ -z "$seo_title" ]]; then echo $divider echo $file echo "seo_title: Empty or not found" have_errors=true elif [[ ${#seo_title} -lt 50 || ${#seo_title} -gt 60 ]]; then echo $divider echo $file echo "seo_title: Length ${#seo_title} (50 <> 60): '${seo_title}'" have_errors=true fi # Check if seo_description is present and valid if [[ -z "$seo_description" ]]; then if [[ $have_errors = false ]]; then echo $divider echo $file fi echo "seo_description: Empty or not found" have_errors=true elif [[ ${#seo_description} -lt 110 || ${#seo_description} -gt 160 ]]; then if [[ $have_errors = false ]]; then echo $divider echo $file fi echo "seo_description: Length ${#seo_description} (110 <> 160): '${seo_description}'" have_errors=true fi # Print divider only if there are no errors # if [[ $have_errors = false ]]; then # echo $divider # fi } echo $divider # Recursive search for all .md and .mdx files in the src/content/docs directory find src/content/docs -type f \( -name "*.md" -o -name "*.mdx" \) | while read file; do # Check if the file contains a seo block before proceeding if grep -q "seo:" "$file"; then check_seo_params "$file" fi done echo $divider
Run the script, navigate to the file, copy the text into GPT, and ask for the optimal title and description. Hereās an example prompt:
Write seo_title and seo_description, send the result in English, use best practices and length requirements for seo. Result in the format: ```yaml seo_title: "" seo_description: ""```
seo_title 50-60 symbols, seo_description 100-160 symbols text is ...
Next, update all pagesāsuper basic, I know, but better than having no metadata or duplicate content.
Favicon Fixes
We messed up the favicon a bitāused realfavicongenerator to generate and test.
OG Tags
I also added OG tagsāmeta tags that make your links look better in social media previews. At the very least, include og:title , og:description , and og:image (use an absolute path). All our images are served via bunny.net, but Iām still fine-tuning that setup.
application/ld+json
Added some structured data with application/ld+jsonāno idea if it works, but it seems cool. Check out structured data markup.
{ tag: "script", attrs: { type: "application/ld+json", }, content: JSON.stringify({ "@context": "https://schema.org", "@type": "WebSite", url: canonical?.href, headline: ogTitle, description: page_description_seo, image: [imageUrl?.href], mainEntity: { "@type": "Article", headline: page_description_seo, url: canonical?.href, dateModified: data?.lastUpdated, image: [imageUrl?.href], author: { "@type": "Organization", name: "ASO.dev", url: "https://aso.dev", }, publisher: { "@type": "Organization", name: "ASO.dev", logo: { "@type": "ImageObject", url: fileWithBase(config.favicon.href), }, }, }, }), },
Custom 404 Page
We built a custom 404 page. Defaulting to the index page is considered an error and could hurt SEO.
Setting the noindex meta tag on the 404 page:
// 404 if (canonical?.pathname === "/404") { headDefaults.push({ tag: "meta", attrs: { name: "robots", content: "noindex", }, }); }
Setting up a custom 404 page in Nginx config:
location / { proxy_redirect off; absolute_redirect off; proxy_set_header Host $http_host; try_files $uri $uri/ =404; # try_files $uri $uri/ /index.html; add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0"; add_header Pragma "no-cache"; add_header Expires "Thu, 01 Jan 1970 00:00:00 GMT"; } # 404 page error_page 404 /404.html; location = /404.html { root /app; internal; }
Redirects
We had a bunch of 301 redirects. We made sure all URLs ended with a trailing slash (e.g., https://aso.dev/aso/ instead of https://aso.dev/aso) to avoid duplication. After a thorough review, we eliminated outdated links in the code and added old URL redirects via NGINX.
# https://aso.dev/app-info/app-info/ https://aso.dev/aso/app-info/ rewrite ^(/ru|/en)?/app-info/app-info/?$ $1/aso/app-info/ permanent;
hreflang
We added x-default to the hreflang attribute. Didnāt know that was a thing until recently.
// Link to language alternates. if (canonical && config.isMultilingual) { for (const locale in config.locales) { const localeOpts = config.locales[locale]; if (!localeOpts) continue; const langPostfix = localeOpts.lang === "en" ? "" : localeOpts.lang; headDefaults.push({ tag: "link", attrs: { rel: "alternate", hreflang: localeOpts.lang, href: localizedUrl(canonical, langPostfix).href, }, }); } headDefaults.push({ tag: "link", attrs: { rel: "alternate", hreflang: "x-default", href: localizedUrl(canonical, '').href, }, }); }
SEMrush
SEMrush
semrush.png 210 KB
Iāve been using SEMrush longerāitās easier to downgrade to a free plan, but 100 checks and their pricing arenāt great.
Backlinks
Getting backlinks is tricky. You need links from reputable sitesāspammy backlinks will hurt you. One good link from the New York Times beats hundreds from random blogs. Weāre building up our backlink profile by listing our site on startup and indie project platforms. We found an Excel sheet with hundreds of link opportunities and are slowly working through it.
Launching on Product Hunt can helpāsubscribe for updates. Weāve delayed the launch a few times, but itās coming soon.
Also, we try to write genuinely useful articles, not just filler.
I mightāve missed something or got things wrong - write in the comments!
š Join WIP to participate