# gen-codes.site — Admin Panel: Deploy & Verify

Read this once before uploading. It takes about 15 minutes end to end.

---

## 1. What changed

| File | State | Purpose |
|---|---|---|
| `server.mjs` | rewritten | App server + the whole admin API (~68 admin routes), auth, CSRF, rate limiting, dynamic `/sitemap.xml`, server-rendered `/` and `/tools.html`, visitor-country lookup, maintenance gate |
| `homepage.mjs` | new | Renders the homepage from the `tools` table — the hero, the numbered category rail, and one generated "featured image" per tool card. No image files, no extra requests |
| `public/admin.html` | rewritten | The admin panel itself — single file, no build step, no CDN dependency |
| `public/sw.js` | rewritten | Was cache-first with no version bump, which would have hidden your admin changes from visitors. Now network-first for HTML; `admin.html` and `/api/` are never cached |
| `seo_data.json` | 3 keys fixed | `text-reverser-vhOG`, `case-converter-BSeY`, `time-zone-converter-CGh` pointed at URLs that do not exist, which is what produced the "3 page(s) not listed" dashboard warning. Renamed to the real slugs |
| `package.json` | `engines` added | Declares Node ≥ 18, which the country lookup needs for global `fetch` |
| `.env.example` | new | Documents every environment variable. Contains no real values |
| `.gitignore` | new | Keeps `.env`, `data/admin.json`, `data/runtime.json` and `sqlite.db` out of git |

`package.json` already lists every dependency needed (`express`, `@libsql/client`, `jsonwebtoken`, `bcryptjs`, `cookie-parser`). Nothing new to install beyond `npm install`.

Nothing under `public/assets/`, `public/blog/`, or `public/page/` was touched. Your 32 blog posts and the compiled React bundle are untouched — `/{slug}.html` still serves the SPA exactly as before, so all 72 tools keep working.

---

## 1b. The design pass, and the four switches that undo it

The header, the menu bar, the band above the footer and the footer itself are now generated in one place — `chromeCss()`, `headerHtml()`, `preFooterHtml()` and `footerHtml()` in `server.mjs` — and every server-rendered page plus every tool page pulls from it. So the whole site shares one design, and changing it means changing one function rather than 70 files.

The one part worth understanding, because it is unusual: **the inside of a tool page.** The compiled bundle themes itself from CSS variables in `:root` that hold HSL channel triplets (`--background`, `--primary`, `--border`, `--radius`, `--app-font-serif`) which its own rules read as `hsl(var(--x))`. There is no `src/` in this repo, so the bundle cannot be rebuilt. Instead the server injects a small stylesheet *after* the bundle's own, re-pointing those variables at this palette. Every card, button, input and heading inside all 72 tools restyles at once, the Georgia serif headings become the site font, and not one line of the app's JavaScript is touched — so no calculator can change behaviour.

Nothing here is one-way. All four of these are checkboxes in **Settings → Homepage**, saved to `settings.json`, effective on the next request, no upload:

| Switch | Off means |
|---|---|
| Show the big heading and sub-heading in the header | *(default off)* header is just the wordmark, tagline and menu bar; the H1 stays in the HTML, hidden |
| Show the "Keep exploring" band above the footer | the four-card band disappears; the footer follows the content directly |
| Use this design on tool pages too | tool pages go back to the compiled app's own header, footer and cream/navy theme |
| Render the homepage on the server | `/` goes back to the original compiled React homepage |

**One fix in this area is deliberately not switchable: navigation on tool pages.** The compiled bundle is a client-side router (wouter). It changes the URL without asking the server for a new page. But the per-tool article, `<title>`, description and canonical are injected by the server *outside* `<div id="root">` — that is what stops React from wiping them. Put together, those two facts caused a real bug: opening one tool and then clicking another swapped the calculator but left the **first tool's article and meta tags** on screen, so every tool appeared to have the same article underneath.

`spaNavScript()` in `server.mjs` fixes it. On any page built from the compiled shell, a small inline script makes same-origin links that change the path perform a real page load, and hooks `pushState`/`replaceState`/`popstate` as a safety net. Middle-clicks, Ctrl/Cmd-clicks, `target="_blank"`, download links, `#anchor` links and external links are untouched. The cost is that moving between tools is a normal page load rather than an instant in-app swap; the gain is that the article, title, description and canonical always match the URL — which is exactly what a search engine (and an AdSense reviewer) checks. It is injected regardless of the "Use this design on tool pages too" switch, and regardless of whether that particular tool has an article, because a tool with no article must not inherit the previous one's either.

**Verify it after restarting:** open `/word-counter.html`, then click a different tool from inside the app. The page should visibly reload, the article underneath should change, and the browser tab title should change with it. Right-clicking a tool link and choosing "Open in new tab" should still work normally.

---

## 2. Deploy on Namecheap (cPanel)

**Step 1 — upload.** Via File Manager or FTP, upload into your app root (the folder containing `package.json`):

```
server.mjs
homepage.mjs
package.json
seo_data.json
public/admin.html
public/sw.js
.env.example
.gitignore
```

**Step 2 — set the environment variables.** Generate a secret on your own machine:

```
node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
```

Then in cPanel go to **Setup Node.js App → your app → Environment variables** and add:

```
JWT_SECRET     = <the long hex string you just generated>
ADMIN_USERNAME = Younasjdw
ADMIN_PASSWORD = <a brand-new password, at least 8 characters>
```

`JWT_SECRET` is required — without it the server invents a new one per process and you get signed out immediately after signing in. That is the third warning on your dashboard.

`ADMIN_USERNAME` / `ADMIN_PASSWORD` are how you skip the setup-code screen you saw. With both set, the server hashes the password into `data/admin.json` at first boot and `/admin.html` shows a plain **username + password** login — no code, nothing else to enter. Delete both variables after the first successful login; the server never reads them again once `data/admin.json` exists.

Do **not** put any of these in a file inside `public/`.

**Step 3 — install and restart.** In **Setup Node.js App**, click **Run NPM Install**, then **Restart**. If you prefer the terminal, activate the virtualenv line cPanel shows you, then `npm install && touch tmp/restart.txt`.

**Step 4 — sign in.** Open `https://gen-codes.site/admin.html`. You will see the animated *Welcome To RAJA Gen Tools Website* wordmark and a username + password form. Sign in with what you set in Step 2.

> **Important:** do not reuse the password you pasted into a chat earlier. Treat it as public. It was never written into any file in this project — the only password that will ever exist on the server is the bcrypt hash of whatever you set. You can change both the username and the password later from **Admin account** inside the panel.

*If you skip `ADMIN_USERNAME`/`ADMIN_PASSWORD`:* the server falls back to a one-time setup code, printed in the cPanel Node app **log** at boot and stored in `data/runtime.json` as `setupCode`. That guard exists so a stranger who finds `/admin.html` before you do cannot claim the account. Setting the two variables is the simpler path.

---

## 3. Verify it for real (the Step 5 checklist)

Do these in order, in a browser. Each one has an exact click path and an exact thing to look at on the public site. If any check fails, the panel is not done — tell me which number.

**1. Creating a record in admin actually appears on the live site.**
Tools → **Add tool** → name `Verify Test`, URL `/verify-test.html`, save. Then open `https://gen-codes.site/tools.html` in a **private window**. `Verify Test` must be listed. Also open `https://gen-codes.site/sitemap.xml` — the slug must appear there.

**2. Editing a record is reflected on next load.**
Edit that tool's name to `Verify Test 2`, save, reload `/tools.html` in the private window. The new name must show.

**3. Deleting or disabling removes it from the live site.**
Toggle its status to **off**. Reload `/tools.html` — it must be gone. Turn it back on, confirm it returns, then use the row menu → **Move to trash**, and confirm it disappears again. Finally **Trash → Restore** or delete it for good.

**4. Refreshing the admin page shows true state, not a cache.**
Hard-refresh `/admin.html` (Ctrl+Shift+R). Every value you just changed must still read the same. The panel never stores records in the browser — every view re-fetches from the server after each write — so a mismatch here means a write silently failed.

**5. Sitemap updates when tools or posts change.**
`/sitemap.xml` is generated per request, ahead of the static file handler, so it cannot go stale. Confirm: Content → **New post**, publish anything, then reload `/sitemap.xml` and find `/blog/<your-slug>.html`. Delete the test post afterwards.

**6. Unauthenticated requests to admin routes are rejected.**
In a private window (never signed in), visit these and confirm you get `{"error":"..."}` with status 401, not data:

```
https://gen-codes.site/api/admin/tools
https://gen-codes.site/api/admin/settings
https://gen-codes.site/api/admin/stats
https://gen-codes.site/api/admin/activity
```

`/admin.html` itself loads for anyone — that is correct and intended, it is just the login screen. It contains no data.

**7. No secrets in client-side code or public files.**
Open `view-source:https://gen-codes.site/admin.html` and search for `secret`, `password`, `JWT`. You should find only field labels. The session token lives in an **httpOnly** cookie that JavaScript cannot read, so there is nothing to leak. Confirm `data/` is not reachable: `https://gen-codes.site/data/admin.json` must 404.

**8. Maintenance mode has a real page.**
Settings → **Maintenance mode → on**. In the private window, load `https://gen-codes.site/` — you must get a branded maintenance page with HTTP 503, not the site. Turn it back off and confirm the site returns. Do this last, and do not leave it on.

**9. All 72 tools are present and editable.**
Tools → the count chip at the top of the table must read 72 (or more, if you have added your own). If it reads fewer, click **Import all from app bundle** — it reads `public/assets/index-*.js`, inserts only slugs that are missing, and never touches a row you have already edited. It also runs automatically at every boot.

**10. The homepage is rendered from the database.**
Open `view-source:https://gen-codes.site/` and search for the title of any tool. It must be there in the raw HTML, not injected by JavaScript — that is what makes it crawlable. Then change one tool's title in the panel and reload `/` in a private window: the card must show the new title. Settings → Homepage lets you change the H1 and the subtitle, and turn the whole server-rendered homepage off again if you ever want the old compiled page back.

**11. Analytics and search-engine verification actually reach the page.**
Analytics & verification → paste your GA4 ID and any verification tokens → **Save**. The **What the server actually injects** card below shows the exact `<head>` block the server is now sending. Confirm it for real: `view-source:https://gen-codes.site/` and search for `G-` and for `google-site-verification`. Then in Google Analytics → Reports → **Realtime**, open the site in another tab and watch your own visit appear. For each search engine, click Verify in its own console.

**12. Visitor countries are being recorded.**
Dashboard → **Visitors by country**. On a brand-new deploy this reads "No country data yet" — that is honest, not broken. Load the site from your phone on mobile data and click the refresh icon on that card; your country must appear. The card itself tells you which source it is using (a CDN header if one exists, otherwise a rate-limited IP lookup). Raw IP addresses are never stored — only a salted hash, for caching.

**13. Username change works and does not lock you out.**
Admin account → **Change your username** → new name + your current password → save. The name in the top bar must update immediately and you must stay signed in. Hard-refresh and sign in again with the new username to confirm it persisted.

**14. The three dashboard warnings are gone.**
Dashboard → Health checks. After Step 2 and this upload, all three should clear: `JWT_SECRET` (set it in cPanel), *3 page(s) not listed* (the `seo_data.json` keys are fixed in this upload), and the unreferenced bundle — for that last one, delete `public/assets/index-pPnXJqLB.js`. It is an old build that nothing links to; it costs disk and confuses crawlers. Check first that `public/index.html` references a different file name, then delete it.

**15. The blog still lists every post.**
Open `https://gen-codes.site/blog/` — all 32 guides must be there. This is the check that catches the regression described below: a `index: false` option on the static file handler silently stopped `/blog/` from resolving to `public/blog/index.html`, so the listing came back empty. That option is gone.

**16. A tool page looks like the rest of the site — inside as well as outside.**
Open any tool, e.g. `https://gen-codes.site/year-progress.html`. Check three separate things. The **header** must be the same wordmark + pill menu bar you see everywhere else, with **Blog**, **About** and **Contact** in it, and the footer must be the dark four-column one. The **interior** — the calculator card, its buttons, its headings, the page background — must be in the same indigo/slate palette as the homepage, *not* the old cream background with navy Georgia headings. And the tool must still **calculate correctly**: change an input and confirm the result updates. That last part matters most, because the interior restyle works by re-pointing the compiled app's own theme variables (`--background`, `--primary`, `--border`, `--radius`, `--app-font-serif`) at this palette from a stylesheet injected after the bundle's own. Nothing inside the app is rewritten, so the maths cannot change — but confirm it anyway on two or three different tools. If anything looks wrong, Settings → Homepage → uncheck **Use this design on tool pages too** and it reverts on the next request, no upload needed.

**17. The article sits above the footer.**
Open `https://gen-codes.site/compound-interest.html` and scroll: the written article must appear between the tool and the footer. Content & SEO now lists all 72 tools with an **Article / No article** badge, so you can see exactly which pages still need one.

**18. The heading is hidden, but the homepage still has an H1.**
Load `https://gen-codes.site/` — above the menu bar you must see only the site name and the tagline. The big *"Free Online Tools for Time, Money…"* heading and its paragraph must be gone, and so must the *"72 free tools — no sign-up required"* line. Then open `view-source:https://gen-codes.site/` and search for `head-h1`: the `<h1>` must still be there, carrying `class="head-h1 sr-only"`. That is deliberate — it is hidden from the eye but still read by Google, so hiding it costs you no SEO. To bring it back visually: Settings → Homepage → tick **Show the big heading and sub-heading in the header**.

**19. The menu bar highlights the page you are on.**
Visit `/`, then `/tools.html`, then `/blog/`, then `/page/about-us.html`. On each one, exactly one pill in the menu bar must be filled indigo — the one matching that page. Each item has a small drawn icon; those are inline SVG, so the nav makes no extra requests. On a narrow phone the pill row wraps instead of overflowing sideways — check that too.

**20. The "Keep exploring" band appears above the footer on every page.**
Scroll to the bottom of `/`, then of `/tools.html`, then of a tool page, then of a blog post. On each you must see four numbered cards in sequence: **01 Latest guides** (your five newest posts, numbered), **02 Browse by category** (every category as a chip), **03 Site & policies** (the five policy pages), **04 Why use it**. Click a card-02 chip from a tool page — it must land you on the homepage scrolled to that category, with the heading clear of the sticky header, not hidden behind it. Then publish a post in Content and reload any page: it must appear at the top of card 01 immediately (the list is cached for five minutes but every blog save clears the cache). Turn the whole band off with Settings → Homepage → **Show the "Keep exploring" band above the footer**.

**21. The footer is the new one, everywhere.**
The footer must be four columns — the site name with three chips, **Explore**, **Popular**, **Company** — over a bottom bar with the copyright and your social links, with the same coloured hairline across the top that the header has. Two things to check that are easy to miss: the social links only appear if you have filled them in under Settings → Social profiles (otherwise you get a single line of text, which is correct, not broken), and on a phone the four columns must collapse to one rather than squashing.

> **One honest exception to 19–21:** the 32 blog posts that already existed under `public/blog/` are static files with their own header and footer baked in from before. `/blog/` (the listing) and any post you create or re-save from the panel get the new chrome; the old files keep their own until you re-save them. Content → open a post → **Save** rewrites it through the current template.


---

## 3b. The warnings you saw, and what each one means

| Warning | What it actually means | Fix |
|---|---|---|
| `JWT_SECRET` is not set | The server generated a random signing key at boot. If the host runs more than one Node process, each has a different key, so a cookie signed by one is rejected by the other — you get signed out at random. | Set `JWT_SECRET` in cPanel → Setup Node.js App → Environment variables, then **Restart**. |
| 3 page(s) not listed | `seo_data.json` had SEO articles keyed to `text-reverser-vhOG`, `case-converter-BSeY` and `time-zone-converter-CGh`. Those URLs do not exist, so the content was attached to pages nobody can visit — wasted, and a soft-404 risk. | Already fixed in this upload; the keys now read `text-reverser`, `case-converter`, `time-zone-converter`. Re-check after restart. |
| Unreferenced asset in `public/assets/` | `index-pPnXJqLB.js` is a leftover from an older build. No HTML file links to it. Harmless, but it is dead weight and crawlers can still find it. | Confirm `public/index.html` points at a different bundle, then delete the file. |

None of the three were breaking the site. The first one is the only one that would have caused visible misbehaviour, and only for you, in the admin panel.

---

## 4. Honest status

Written, wired, and read line by line against the API it calls, then reviewed a second time file-by-file for undefined identifiers, mismatched SQL placeholders and broken handler bindings. The design pass got the same treatment: every class name emitted by the header, the pre-footer and the footer was checked against the stylesheet that ships with it, every call site of the four chrome helpers was listed and checked, and every settings key the panel sends was matched against the server's whitelist so nothing silently resets on save. **Not runtime-verified**, because the Linux sandbox in this session was down for its entire duration (`VM_DISK_SPACE_INSUFFICIENT`), so I could not boot the server, run `node --check`, or click a single button. Every claim above describes what the code does, not something I watched happen. Sections 3 and 3b exist precisely so you can convert "should work" into "does work" in about twenty minutes.

Four things I want to flag rather than bury:

**All 72 tools now have a real article — this is the fix for the second AdSense "Low value content" rejection.** `seo_data.json` previously held content for 23 slugs, and those 23 were themselves part of the problem: every one shared a byte-identical inline `<style>` block and the same three-heading skeleton ("What is X / Why Use Our Online X / Frequently Asked Questions"), the thinnest being `regex-tester` at roughly 90 words. That is precisely the thin-content and templated-content pattern the rejection names.

Both halves are now done. The 49 missing articles were written, and the original 23 were **rewritten from scratch** — not edited. Every one of the 72 now carries a real formula in `<code>`, a worked example with the arithmetic checked, at least one HTML table of concrete values, an honest section on what the tool cannot know (with medical, financial or legal non-advice wording where the subject calls for it), and three or four specific FAQs. No two share a structure, and most correct a piece of folklore in their own niche rather than repeating it. Several correct claims their own previous version made — the compound-interest page no longer attributes an invented quote to Einstein, the Lorem Ipsum page no longer repeats the unverified "since the 1500s" printer story, the CSS gradient page explains why adding vendor prefixes now *breaks* the angle rather than helping, and the text-reverser page states plainly that reversing a password provides no protection.

Two mechanical rules were followed throughout, and matter if you ever edit these by hand: **no inline `<style>` block** (the `.gc-article` rules live in `spaChromeCss()` and the server already wraps the blob in `<div class="gc-article">`), and **no double quotes and no backslashes anywhere inside an article** so JSON escaping cannot break. Verified after the last edit: 72 keys, 72 single-line article values, zero internal double quotes, zero stray backslashes.

Content & SEO still shows the **Article / No article** badge per tool, so you can confirm all 72 read "Article" after the restart.

**Four thin blog posts were consolidated, not deleted.** The tool articles were only half the "low value content" exposure — the blog had two generations of post, and four early files (`age-calculator.html`, `base64-encoder.html`, `password-generator.html`, `word-counter.html`) still used the same generic "What is X / Why use it / How to use it / FAQ" skeleton *and* each duplicated a topic a later, much deeper post already covers. Thin plus near-duplicate is exactly the pattern the review names, so each one now issues a permanent redirect to its replacement:

| Old URL | Redirects to (301) |
| --- | --- |
| `/blog/age-calculator.html` | `/blog/how-the-age-calculator-works.html` |
| `/blog/base64-encoder.html` | `/blog/base64-encode-decode-guide.html` |
| `/blog/password-generator.html` | `/blog/how-to-use-password-generator.html` |
| `/blog/word-counter.html` | `/blog/word-counter-guide.html` |

The four files stay on disk — nothing was deleted — but a `RETIRED_POSTS` map in `server.mjs` is the single source of truth for three things at once: the 301 route (registered **above** `express.static`, or the old file would be served instead), the sitemap builder (retired URLs are skipped), and the pre-footer "latest guides" list. They cannot drift apart. The blog index lost those four cards (28 unique post links remain, no duplicates), and every internal `related-card` link that pointed at a retired URL was repointed to the replacement — or, where the link would have become a self-redirect, swapped for a genuinely different and more topically relevant post. `public/sitemap.xml` still lists the old URLs on disk and needs no edit: the dynamic `/sitemap.xml` route rewrites that file as a side effect, so it self-heals on the first request after the restart.

**Verify it after restarting:** open `https://gen-codes.site/blog/word-counter.html` and confirm the browser lands on `/blog/word-counter-guide.html` (a 301, not a 302 — check the Network tab if you want to be sure), then load `/sitemap.xml` and confirm none of the four old URLs appear. Then open `/blog/` and confirm 28 cards with no repeated titles.

**The homepage grid is now editable — by replacing the page, not by editing the bundle.** `public/` holds a compiled Vite bundle with no `src/` in this repo, and the old homepage's tool cards were string literals inside `assets/index-*.js` — a bundle that makes zero `/api` calls. No admin panel could ever have edited that, which is the real reason the previous panel felt fake. So `/` and `/index.html` are now rendered by `homepage.mjs` from the `tools` table instead: hero, numbered category dropdowns in the rail, generated card artwork, JSON-LD, and a search box, all in HTML that arrives complete. Everything else is untouched — `/{slug}.html` still serves the SPA, so all 72 tools work exactly as before. This was my architectural call, not something you asked for specifically, and it is reversible: Settings → Homepage → turn off **Server-rendered homepage** and the compiled page comes back on the next request.

**The card "featured images" are drawn, not uploaded.** Each card gets a CSS gradient keyed to its category, one inline SVG glyph, a decorative blob positioned from a hash of the slug, and a ghosted monogram. That means 72 distinct-looking cards and **zero image requests** — nothing to compress, nothing to lazy-load, no effect on Core Web Vitals. If you want a real photo on a specific tool, put the file in Media and set the tool's **Featured image** field; that one card then uses your image and the rest keep the generated art.

**Whole-file HTML editors and the extra head-code box are deliberately not sanitized.** Rich-text bodies, new blog posts, and SVG uploads are sanitized. But the raw blog/page editors and Analytics → **Extra head code** save what you type verbatim, because those legitimately contain your AdSense script and JSON-LD and stripping them would break monetisation. Only the signed-in admin can reach them, and the panel says so in a banner above each one.

One more limit worth knowing: the head-code block (GA4, verification tags, AdSense) is injected into pages the **server** renders — `/`, `/tools.html`, the SPA shell, and blog/page routes. Your 32 pre-built files under `public/blog/` already carry their own tags from before; if you change the GA4 ID, use SEO → **Sync codes into static files** to rewrite the managed block inside them.

---

## 5. If something goes wrong

**"Security token mismatch."** Your session cookie and CSRF cookie disagree, usually after a server restart. The panel retries once automatically; if it persists, hard-refresh and sign in again.

**Signed out immediately after signing in.** `JWT_SECRET` is changing between requests — that happens when the host runs more than one Node process and the variable is not set, so each process invents its own. Set `JWT_SECRET` in cPanel and restart.

**Tools save but `/tools.html` is empty.** The database is not writable or not shared. Check Dashboard → the DB chip reports the mode and any error. If the host runs multiple processes, a single local `sqlite.db` file is not safely shared — move to Turso by setting `DATABASE_URL` and `DATABASE_AUTH_TOKEN` (see `.env.example`).

**Visitors still see the old page after an edit.** A stale service worker from before this deploy. The new `sw.js` bumps the cache name to `gencodes-v3` and deletes older caches on activation, so it self-heals on the next visit. To force it now: Settings → **Clear caches**, then hard-refresh.

**Locked out of login.** The login route rate-limits by IP. Wait out the countdown in the error message — it tells you the exact number of seconds.

**Truly locked out.** Delete `data/admin.json` on the server and restart. The first-run screen returns with a fresh setup code in the log. Your content is untouched; that file holds only the admin username and password hash.
