How I built PolishPal, and how I run it now
From the first commit on 18 June 2026 to a routine of keyword rounds, scheduled publishing, native-speaker checks and a monitoring dashboard.

In short
- The first commit (18 June 2026) already had lessons, quizzes and progress tracking; Polaris, the custom CMS, followed nine days later.
- Articles live in a Supabase database and images are committed as WebP; a daily server job publishes due drafts only after checking every image.
- Topics come from numbered keyword rounds; since 5 November they use DataForSEO search volumes.
- A native Polish speaker's review led to re-recording all 744 audio lines with a voice forced to speak Polish.
- Content Monitor tracks schedule runway, Google index status, Search Console traffic and internal links, with Telegram alerts.
- Most lessons came from failures: undecodable images, previews that can't build, small hero images and an outdated About page.
PolishPal is a free website for learning Polish in English. I build and run it on my own, as Scentro. This post covers how it started, what it runs on, and the routine that keeps it going now: research, writing, publishing, checking and fixing. The PolishPal project page shows the finished product.
What PolishPal is and who it is for#
PolishPal (polishpal.pl) teaches complete beginners, from A0 to A1. There are 16 short lessons in a fixed order, 16 quizzes with instant feedback and 15 grammar topics with declension and conjugation tables. Progress is saved in the browser, so there is no account to create.
Every Polish line on a lesson page has a speaker button. Behind those buttons are 744 pre-recorded audio files, and the playback speed goes from 0.6× to 1.25×.
Next to the course sits a blog with more than 100 articles. They answer the questions people actually type into Google: how to say thank you, what Żabka is, how Wigilia works, what a PESEL number is. Since November 2026 there are also video lessons and Shorts on YouTube, built from the same lesson content.
The readers are English speakers who are starting Polish from zero. That includes people moving to Poland, travellers and people with Polish family. The blog topics reflect that mix: everyday words, expat paperwork, food, holidays and heritage topics such as Polish surnames and family words.
How it started#

The first commit is dated 18 June 2026. It already contained the core of the course: lesson pages, quizzes and progress tracking stored in the browser, on Next.js. Grammar topics followed the same evening. The next two days added search, a telling-the-time lesson and a speaker button, which at first used the browser's built-in speech.
The first week stayed busy:
- 24 June: dark mode, an accessibility panel and reworked tables.
- 25 June: star ratings and comments on Supabase, plus a sitemap and robots file. The domain moved from polishpal.app to polishpal.pl.
- 26 June: the first blog posts, still as Markdown files in the repository, and a written authoring guide.
- 27 June: Polaris, my own CMS inside the site. It has a post editor, an SEO panel and internal link suggestions. From then on articles live in the database, not in the code.
July was the big writing month: 77 articles went live. The CMS grew alongside them. It got automatic backups before every save (5 July), IndexNow pings on publish (7 July) and an index checker that asks the Search Console URL Inspection API about each page (11 July).
August added nine more articles. Then nothing new was published until late October, while I worked on a full redesign. That redesign was merged on 24 September. What happened to search traffic afterwards gets a separate post, with the data.
The stack#
Nothing exotic: every piece is something I can fix myself.
- Next.js on Vercel. Article pages are cached and refreshed at most once an hour.
- Supabase (Postgres) for articles, comments and ratings. The database is in Stockholm, so the site's server functions run in the same region. When they ran in the US, the blog listing was noticeably slower.
- ElevenLabs for the Polish recordings, stored as static MP3 files. If a file is missing, the browser's own speech takes over.
- GSAP for Żubek, the bison-calf mascot. He is an SVG puppet that moves his mouth in time with the audio.
- Resend for the newsletter, with double opt-in.
- Postiz, self-hosted, for scheduling social posts.
- Google Search Console and DataForSEO for search data.
Much of the code and the first drafts are written with AI coding agents. They work from rules kept in the repository: an authoring guide, a list of mistakes to avoid, and checks that have to pass. Since September most changes arrive as pull requests that say what changed and how it was checked.
The content system#

Polaris, the CMS#
Each article is a database row: title, slug, excerpt, meta description, focus keyword, category, a short summary, tags and the Markdown body. Images are not uploaded to the CMS. They are committed to the site as WebP files. Content lives in the database and images live in git, and that split decides how everything else works.
Keyword research in rounds#
Topics come from numbered research rounds. Round 31, on 5 November, was the first to use DataForSEO search volumes instead of my own judgement of demand. An earlier round had recommended posts that were already live, so every round now starts by checking the live posts and the sitemap.
When the DataForSEO plan was ready, I took 17 queued posts from the previous round off the schedule so the new plan could go first. The drafts stayed in the CMS, and nothing was deleted.
A schedule that runs without me#
The CMS has no "publish at" field. Instead, a small JSON file in the repository lists slugs and dates. A daily server job at 08:00 Warsaw time publishes every draft that is due.
Before it publishes, the job checks that every image the article uses is live and is a valid WebP. Since November it also checks that the main image is at least 1200 px wide and has its share-image crops. If anything fails, the post waits. After publishing, the job refreshes the blog and notifies search engines through IndexNow.
Until 27 October this ran on my own computer. I moved it to the server because publishing should not depend on a laptop being switched on. The current plan is 16 posts between 6 November and 21 December, one every three days.
SEO checks and internal links#
On 4 November a site scan found that some pages had lost their share image and others had inherited the homepage's title. Next.js merges page metadata shallowly, so one section that set its own Open Graph tags silently dropped the rest. Now every page builds its metadata through one helper, which always outputs the full set.
Internal links get their own rounds. On 4 November I added 115 links across 58 posts. On 6 November I added 140 more across 54 posts, all pointing at 50 posts that Google had crawled but not indexed.
All 50 of those posts passed the technical checks. My working conclusion was that they were simply poorly linked. Matching existing phrases found only about 40 of the roughly 145 links needed. The rest needed a new bridging sentence, written by hand into the text.
Getting the Polish right#
A course that teaches wrong Polish is worse than no course, so this part gets the most care.
On 6 October an accuracy pass over every lesson, quiz and grammar page produced 68 content fixes and two quiz bugs. Some examples:
- "21 books" had been taught as dwadzieścia jedna książka instead of dwadzieścia jeden książek.
- A stress example contradicted its own rule.
- The accusative of one was given as ich instead of je.
- A fill-in-the-blank quiz accepted answers typed without Polish letters, even when that spelling is a real but different word. Those fallbacks were removed. Tap-to-insert buttons for ą, ć, ę, ł, ń, ó, ś, ź and ż were added under the answer box instead.
The audio went through a review of its own. The first voice, recorded on 7 November, replaced the browser's robot voice. A native Polish speaker then listened through lessons 1 to 6 and found real problems. The model was guessing the language, so my, zero and komputer came out in English, and some words ended with a stray "mmm".
All 744 lines were re-recorded with a different voice on a model that is forced to speak Polish. She picked that voice from five options. Every clip was checked with Polish speech-to-text, and misses were recorded again. Sound rows such as sz or rz now play an example word, because no voice can say a lone sound well. The editorial policy states that the audio is an AI voice.
The same rule applies to video. Any new Polish line in a YouTube lesson waits for the native check before the video is scheduled.
How I monitor it#
On 4 November I built Content Monitor, a dashboard for all my content sites. For PolishPal it shows:
- Every article and the schedule, with the runway: how many days of scheduled posts are left. When nothing is scheduled, it raises an alert.
- Google index status for each page, from the URL Inspection API. Pages that are not indexed are checked again after three days, and indexed pages after 21.
- Search Console traffic, synced daily, plus a keyword view that highlights quick wins at positions 4 to 20.
- A link crawler that flags orphan posts, posts with fewer than three internal links, and broken links.
Alerts go to Telegram. When a new article goes live, the monitor checks its index status straight away and then rechecks it daily for two weeks. The message says whether Google has indexed it.
One early lesson was to never send a status from a stored result. A test message called a post "not indexed" when Google had indexed it hours earlier. Another was that Search Console only accepted four manual indexing requests on 6 November before showing "Quota exceeded", so I now plan those in batches of five a day.
Social: Facebook and YouTube#
Social posts are written in batches and scheduled through Postiz. The PolishPal Facebook page started on 7 November.
YouTube lessons are built from the lesson data. Each video is a set of scripted scenes rendered to video, with an English narrator. The Polish lines reuse the site's approved recordings, so the channel and the course always say the same thing.
Lesson 1 went out on 9 November and now plays on its lesson page, alongside a "Review in 30 seconds" set of Shorts. Lessons 2 to 4 are scheduled. The pilot cost about $1.70 in voice and video credits.
What I would do differently#
- Self-host and validate images from day one. The CMS upload endpoint produced WebP files that returned 200 but could not be decoded, so browsers showed a broken image. On 2 November a sweep found 12 missing images and 108 image references in 30 posts that were missing their folder path. Now every image sits in the site's own /blog/ folder, a script decodes all of them, and the scheduler refuses to publish a post with a bad one.
- Know what your previews can do. The Supabase settings exist only for production, so every branch preview fails at build time. Changes are visible only after they merge.
- Use large images from the start. 59 published posts had a main image under 1200 px wide, and the share image was a text card. Google recommends images at least 1200 px wide for Discover. Fixing that took new photos, 16:9 crops for all 132 posts and a gate in the scheduler.
- Keep the trust pages honest. The About page said "no ads" while the blog showed ads after consent. It was corrected on 8 November. Legal and trust pages drift quietly, so they need checking whenever the site changes.
- Choose the licence early. PolishPal launched as CC0, public domain. On 10 November it moved to all rights reserved. CC0 cannot be withdrawn for copies people already took, so the earlier choice still applies to them.
- Never link to a post before it is live. If anyone opens the address before the article exists, the cache can keep a "not found" page for up to an hour.
What is next#
The near-term list is short: the next keyword round, more video lessons at about two a week, and a named author byline on the articles. The routine stays the same: write it down, check it automatically, and fix the cause rather than the symptom.