Skip to content

Refactor to SSG pre-rendering with vite-ssg#297

Merged
optout21 merged 3 commits intohuszonegy:mainfrom
joelklabo:ssg-prerendering
Feb 10, 2026
Merged

Refactor to SSG pre-rendering with vite-ssg#297
optout21 merged 3 commits intohuszonegy:mainfrom
joelklabo:ssg-prerendering

Conversation

@joelklabo
Copy link
Contributor

@joelklabo joelklabo commented Feb 7, 2026

Summary

Resolves #221 — Refactors the site from a client-rendered SPA to static site generation (SSG) using vite-ssg.

Before: dist/index.html was 1.6 KB with an empty <div id="app"> — search engines saw no content.

After: All 14 pages are pre-rendered at build time with full HTML content (e.g., index.html is now 13.6 KB with real page content).


⚠️ BREAKING CHANGE: Node.js 20+ Required

This PR upgrades the build environment from Node 18 to Node 20.

  • Why: vite-ssg@28.3.0 and its dependencies (jsdom, undici, etc.) require Node 20+
  • Impact: Node 18 reached EOL in April 2025 and is no longer supported
  • Changes:
    • CI workflows updated to use Node 20
    • package.json updated: @tsconfig/node20, @types/node@^20
    • tsconfig.node.json updated to extend @tsconfig/node20

Local development: If you're on Node 18, upgrade to Node 20+ before running npm install.


Changes

  • package.json: Added vite-ssg and @unhead/vue; changed build script from vite build to vite-ssg build; upgraded Node-related devDeps to v20
  • src/main.ts: Replaced createApp() with ViteSSG() — exports a function that vite-ssg calls for each route during build
  • src/router/index.ts: Exported routes array (vite-ssg creates its own router instance)
  • src/App.vue: Removed unused RouterLink import
  • vite.config.ts: Added SSR config for Bootstrap compatibility
  • tsconfig.node.json: Updated to extend @tsconfig/node20
  • CI workflows: Updated to use Node 20

How it works

At build time, vite-ssg renders each route to static HTML. The output is plain .html files that contain the full page content. On the client, Vue hydrates the existing HTML and the site continues to work as a normal SPA (client-side navigation, Bootstrap collapse, GoatCounter tracking).

Pre-rendered pages (14 total)

Route File Size
/ index.html 13.6 KB
/cikk cikk.html 36.5 KB
/hir hir.html 74.0 KB
/podcast podcast.html 131.2 KB
/eloadas eloadas.html 59.6 KB
/oktatovideo oktatovideo.html 21.0 KB
/konyv konyv.html 23.7 KB
/tarcak tarcak.html 20.9 KB
/link link.html 10.1 KB
/forum forum.html 6.3 KB
/meetup meetup.html 7.0 KB
/tamogatas tamogatas.html 7.1 KB
/pizzaday pizzaday.html 9.3 KB
/support support.html 7.1 KB

Deployment

Compatible with GitHub Pages — the dist/ folder contains static HTML files that can be deployed via simple copy. The existing GitHub Actions workflow should work with the updated build script.

Verification

npm install
npm run build   # type-check + vite-ssg build
# Check dist/index.html — should contain real HTML content, not just <div id="app">

Resolves huszonegy#221: Convert SPA to static site generation so that
deployed HTML files contain actual content instead of empty
placeholders filled by JavaScript.

Changes:
- Add vite-ssg and @unhead/vue dependencies
- Refactor main.ts to use ViteSSG() instead of createApp()
- Export routes array from router/index.ts for vite-ssg consumption
- Update build script to use vite-ssg build
- All 14 pages are now pre-rendered at build time with full HTML content

The site still works as a Vue SPA after hydration (client-side
navigation, Bootstrap collapse, GoatCounter tracking), but search
engines now see the complete content without executing JavaScript.
@optout21
Copy link
Collaborator

optout21 commented Feb 8, 2026

Looks good at first glance.

Trying locally I got an error, after npm install and npm run build. It looks like the CI got also the same error:

Error [ERR_REQUIRE_ESM]: require() of ES Module /home/runner/work/huszonegy.github.io/huszonegy.github.io/node_modules/@exodus/bytes/encoding-lite.js from /home/runner/work/huszonegy.github.io/huszonegy.github.io/node_modules/html-encoding-sniffer/lib/html-encoding-sniffer.js not supported.
Instead change the require of encoding-lite.js in /home/runner/work/huszonegy.github.io/huszonegy.github.io/node_modules/html-encoding-sniffer/lib/html-encoding-sniffer.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/home/runner/work/huszonegy.github.io/huszonegy.github.io/node_modules/html-encoding-sniffer/lib/html-encoding-sniffer.js:2:41)
    at Object.<anonymous> (/home/runner/work/huszonegy.github.io/huszonegy.github.io/node_modules/jsdom/lib/api.js:7:27) {
  code: 'ERR_REQUIRE_ESM'
}

Node.js v18.20.8
ERROR: "build-only" exited with 1.

Could you have a look @joelklabo ? May be some versioning issue)?

@optout21 optout21 requested a review from openoms February 8, 2026 12:09
@joelklabo
Copy link
Contributor Author

Let me check

- Update CI workflows (test-deploy.yml, gh-pages-deployment.yml)
- Update package.json devDeps (@tsconfig/node20, @types/node@^20)
- Update tsconfig.node.json to extend @tsconfig/node20

BREAKING: Requires Node 20+ to build (Node 18 reached EOL April 2025)
@joelklabo
Copy link
Contributor Author

I see, looks like you're supporting Node 18, I'll add the change to update that to 20 in the PR.

Unless you need that version specifically?

@optout21
Copy link
Collaborator

optout21 commented Feb 8, 2026

I see, looks like you're supporting Node 18, I'll add the change to update that to 20 in the PR.
Unless you need that version specifically?

Sure, that's fine, please do.

@joelklabo joelklabo marked this pull request as ready for review February 8, 2026 16:20
Copy link
Collaborator

@openoms openoms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Joel, thank you for the PR!

Will need some changes as per Claude Opus 4.6:

PR Review

Good migration overall — the vite-ssg integration, Bootstrap SSR handling, and CI updates are all correct. One critical gap before merge:

🔴 SEO head tags are not rendered into pre-built HTML

The old router.beforeEach that set document.title and injected <meta> tags was removed, but @unhead/vue (already installed) is never called. Every pre-rendered page will share the same generic <title> from index.html, defeating the purpose of SSG for SEO.

Fix: Create src/composables/useRouteHead.ts:

import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'
import { computed } from 'vue'

export function useRouteHead() {
  const route = useRoute()
  useHead({
    title: computed(() => (route.meta.title as string) || 'HUSZONEGY - csak bitcoinról, magyarul'),
    meta: computed(() =>
      ((route.meta.metaTags as Array<{ name: string; content: string }>) || []).map((t) => ({
        name: t.name,
        content: t.content,
      }))
    ),
  })
}

Then call useRouteHead() once in App.vue's <script setup>. This makes vite-ssg render unique <title> + <meta> per page at build time, and keeps them reactive on client navigation.

Verify: grep '<title>' dist/cikk.html should show Bitcoin cikkek magyarul - HUSZONEGY, not the generic fallback.

🟡 Minor items

  • GoatCounter typing: Replace (window as any).goatcounter with a proper declare global in a src/global.d.ts — restores type safety the old code had.
  • Enforce Node 20: Add "engines": { "node": ">=20" } to package.json so npm install fails fast on Node 18 instead of producing cryptic errors.
  • 404 route: Consider adding /:pathMatch(.*)* catch-all so vite-ssg pre-renders a 404.html (GitHub Pages picks it up automatically).

The useHead() fix is the blocker — the rest can be follow-ups.

- Wire up @unhead/vue via useRouteHead() composable so each
  pre-rendered page gets its own <title> and <meta> tags at build time
- Add global.d.ts for GoatCounter typing (replaces `window as any`)
- Add "engines": { "node": ">=20" } to package.json
- Add /404 catch-all route with NotFoundView, pre-rendered by vite-ssg
- Remove manual cp index.html→404.html workaround from deploy workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@joelklabo
Copy link
Contributor Author

All review items addressed in 49979f7:

SEO head tags (blocker): Created src/composables/useRouteHead.ts using @unhead/vue's useHead(), called from App.vue. vite-ssg already sets up createHead() internally (useHead: true by default), so this was all that was needed. Verified:

$ grep '<title>' dist/cikk.html
  <title>Bitcoin cikkek magyarul - HUSZONEGY</title>

$ grep 'meta name="description"' dist/cikk.html
  <meta name="description" content="Bitcoin cikkek, elemzések és tanulmányok magyarul...">

GoatCounter typing: Added src/global.d.ts with a GoatCounter interface and Window augmentation. Replaced (window as any).goatcounter with typed window.goatcounter.

Node 20 enforcement: Added "engines": { "node": ">=20" } to package.json.

404 route: Added /404 route with NotFoundView + catch-all /:pathMatch(.*)* redirect. vite-ssg now pre-renders dist/404.html (5.49 KiB) directly, so removed the manual cp index.html 404.html workaround from the deploy workflow. GitHub Pages picks up 404.html automatically.

Build passes, 15 pages rendered including 404.

Copy link
Collaborator

@optout21 optout21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! CI is OK, tested locally. Other reviewer's concerns seem addressed. I still need some confirmation that it works fine with the GH Pages deployment, but I don't see a reason why it should not work.

@optout21
Copy link
Collaborator

optout21 commented Feb 9, 2026

Hey @joelklabo , I'd like to accept your big on SatShoot.com, but I can't see it. Something strange, as I got DM about it. Sent you some Nostr DMs, please check.

@joelklabo
Copy link
Contributor Author

joelklabo commented Feb 9, 2026

Hey @joelklabo , I'd like to accept your big on SatShoot.com, but I can't see it. Something strange, as I got DM about it. Sent you some Nostr DMs, please check.

Ah! It looks like you can't see it because a web of trust Nostr thing. That's a pretty new account.

Here's my original npub of you want to check: npub19a86gzxctwtz68l8zld2u9y2fjvyyj4juyx8m5geylssrmfj27eqs22ckt

I will take a screenshot of the bid

@joelklabo
Copy link
Contributor Author

Hey @joelklabo , I'd like to accept your big on SatShoot.com, but I can't see it. Something strange, as I got DM about it. Sent you some Nostr DMs, please check.

image

@optout21 optout21 merged commit 6710f0a into huszonegy:main Feb 10, 2026
1 check passed
@optout21
Copy link
Collaborator

@joelklabo , can you check your Nostr DMs (about payment)?
The change looks good, thanks!

@optout21
Copy link
Collaborator

Update: job paid through the https://satshoot.com platform

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor to SEO-friendly server-side pre-rendering

3 participants