The SEO I actually ship on NeverCram

Most SEO advice I read is written for sites I will never run. NeverCram is a spaced-repetition study app with a blog bolted on, and the four moves that moved its traffic are almost boring: honest metadata, OpenGraph cards that look decent when pasted into a Discord server, Article structured data, and narrow long-tail posts that answer questions my users actually type. That is the full list.

Metadata, but only the parts Google reads

Titles still matter. Descriptions do not rank, but they control what shows up under the title in search results, so they drive clicks. The keywords meta tag is dead, and I have not touched it in years.

I cap titles at 60 characters and descriptions at 155. NeverCram's blog posts are generated from MDX, and the caps are enforced in the metadata export:

export const metadata: Metadata = {
  title: 'How FSRS-6 schedules your next review',
  description:
    'FSRS-6 picks your next review by fitting stability and difficulty to each card. Here is what that means when you study twice a week.',
}

Two things I got wrong for months. I was letting Next.js append the site name to every title, which pushed most of them past 60 characters and got them truncated in the SERP. Fixed that with a titleTemplate that only appends when the page does not already include the brand. Second, my descriptions kept restating the title. A description that restates the title gives the reader no new information to click on.

OpenGraph cards did more for referral traffic than any keyword work

The traffic that got NeverCram off the ground in the first months did not come from Google. It came from people pasting links into Discord servers and WhatsApp study groups. Both surfaces scrape OpenGraph.

I ship a default OG image for the app and override it per post with a generated image:

export const metadata: Metadata = {
  openGraph: {
    title: article.title,
    description: article.description,
    type: 'article',
    url: `https://nevercram.app/blog/${slug}`,
    images: [`/api/og?title=${encodeURIComponent(article.title)}`],
  },
  twitter: { card: 'summary_large_image' },
}

The /api/og route renders an image at request time with the post title on the NeverCram gradient. Nothing fancy. What matters is that the card stays legible on a phone, the title reads without squinting, and the image is close to 1200×630 so no platform crops it awkwardly.

Before I shipped any of this I ran every route through Facebook's sharing debugger and Twitter's card validator. Any time I updated an OG image, I forced a rescrape with the debugger so the cached version was not still pointing at the old file. That one step has saved me two separate "why is my hero image the old one" afternoons.

Structured data: ship Article + BreadcrumbList and move on

For the blog I emit only two JSON-LD blobs per post: Article and BreadcrumbList. Enough for Google to render rich results where it wants, and not so much that I am maintaining a schema catalogue.

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'Article',
      headline: article.title,
      datePublished: article.date,
      dateModified: article.dateModified ?? article.date,
      author: {
        '@type': 'Person',
        name: 'Rahul Ragi',
        url: 'https://rahulragi.com',
      },
      image: `https://nevercram.app/api/og?title=${encodeURIComponent(
        article.title,
      )}`,
    }),
  }}
/>

On Rabbitholes.ai the story is different. The product is an infinite canvas where every node is its own chat, so most of the app is behind auth and not indexable anyway. The only SEO surface that mattered there was the public docs site and the marketing landing. The same template works: Article on doc pages, Organization on the home page, a sitemap that only exposes public routes.

Long-tail content beats keyword stuffing, by a margin I did not expect

This is the section that took me longest to internalise. I used to write around a broad keyword like "spaced repetition" and wonder why none of the posts ranked. They were competing with Anki's wiki, SuperMemo's old essays, and years of study-subreddit threads. There was no angle on the head term where a small blog was going to win.

What did work was writing narrowly. Posts like "Why FSRS marks your cards as lapsed when you feel fine about them" or "Setting FSRS-6 parameters if you only study twice a week" have almost no search volume on their own. They also have almost no competition, and the people searching for them are exactly the audience I want on NeverCram.

So I stopped picking topics by keyword volume. I started picking them from NeverCram's own support tickets and Discord messages. If three different users asked a variation of the same question in a week, that question got a post. Those posts rank because they are written in the phrasing real users are searching in.

The loop I follow now:

  1. Pull a real user question from support or Discord
  2. Write the post in one sitting, first person, 900–1,200 words
  3. Read the top three Google results for the target phrase, only to make sure I am saying something they are not
  4. Link the post to the in-app setting or page it describes

The last step is the one I skipped for the first six months and it turned out to be the highest-leverage of the four. Internal links from a blog post to an actual product page pass a bit of link equity, and more importantly they catch the reader at the moment they are curious. Adding them to every existing post in an afternoon was the one change where I could see a visible bump in the blog-to-signup funnel without a tool telling me about it.

What I skipped on purpose

The SEO material I was reading had a whole section on backlink building: guest posts, link velocity, disavow files, competitor analysis in Spyfu. I skipped all of it for NeverCram.

The reason is unsexy. For a small indie app in its first year, the return on backlink work is bad. A single Reddit post on r/Anki or a mention in a study-adjacent newsletter drives more qualified traffic than a month of outreach, and the outreach takes real time. I would rather spend that time on product and on posts that people actually want to read.

I will revisit backlinks the day NeverCram hits an organic ceiling that only link authority can break through. It has not hit that ceiling yet, so the folder stays empty.

The two-minute check I run before every post ships

Before a post goes live I open four tabs: the preview URL, Twitter's card validator, Facebook's sharing debugger, and Google's Rich Results Test. If all four look right, the post ships. If one looks wrong, I fix the metadata and rescrape before I hit publish. That is the entire pre-flight. It takes about two minutes and it has caught every OG mistake I have ever made.

The next post I write will almost certainly break one of the four checks anyway. That is fine. The point of having a checklist is that I find out in two minutes instead of three weeks.