Refactor to SSG pre-rendering with vite-ssg#297
Conversation
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.
|
Looks good at first glance. Trying locally I got an error, after Could you have a look @joelklabo ? May be some versioning issue)? |
|
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)
|
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. |
openoms
left a comment
There was a problem hiding this comment.
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).goatcounterwith a properdeclare globalin asrc/global.d.ts— restores type safety the old code had. - Enforce Node 20: Add
"engines": { "node": ">=20" }to package.json sonpm installfails fast on Node 18 instead of producing cryptic errors. - 404 route: Consider adding
/:pathMatch(.*)*catch-all so vite-ssg pre-renders a404.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>
|
All review items addressed in 49979f7: SEO head tags (blocker): Created GoatCounter typing: Added Node 20 enforcement: Added 404 route: Added Build passes, 15 pages rendered including 404. |
optout21
left a comment
There was a problem hiding this comment.
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.
|
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 , can you check your Nostr DMs (about payment)? |
|
Update: job paid through the https://satshoot.com platform |

Summary
Resolves #221 — Refactors the site from a client-rendered SPA to static site generation (SSG) using vite-ssg.
Before:
dist/index.htmlwas 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.htmlis now 13.6 KB with real page content).This PR upgrades the build environment from Node 18 to Node 20.
vite-ssg@28.3.0and its dependencies (jsdom, undici, etc.) require Node 20+package.jsonupdated:@tsconfig/node20,@types/node@^20tsconfig.node.jsonupdated to extend@tsconfig/node20Local development: If you're on Node 18, upgrade to Node 20+ before running
npm install.Changes
package.json: Addedvite-ssgand@unhead/vue; changed build script fromvite buildtovite-ssg build; upgraded Node-related devDeps to v20src/main.ts: ReplacedcreateApp()withViteSSG()— exports a function that vite-ssg calls for each route during buildsrc/router/index.ts: Exportedroutesarray (vite-ssg creates its own router instance)src/App.vue: Removed unusedRouterLinkimportvite.config.ts: Added SSR config for Bootstrap compatibilitytsconfig.node.json: Updated to extend@tsconfig/node20How it works
At build time, vite-ssg renders each route to static HTML. The output is plain
.htmlfiles 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)
/index.html/cikkcikk.html/hirhir.html/podcastpodcast.html/eloadaseloadas.html/oktatovideooktatovideo.html/konyvkonyv.html/tarcaktarcak.html/linklink.html/forumforum.html/meetupmeetup.html/tamogatastamogatas.html/pizzadaypizzaday.html/supportsupport.htmlDeployment
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