✨ feat: enhance SSR support with language extraction, dynamic page titles, and updated styles; adjust color theme
This commit is contained in:
@@ -65,7 +65,7 @@ make docker-down
|
|||||||
# "make help" zeigt alle Kommandos
|
# "make help" zeigt alle Kommandos
|
||||||
```
|
```
|
||||||
|
|
||||||
| UI | URL |
|
| UI | URL |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Website | <https://tibi-svelte-starter.code.testversion.online/> |
|
| Website | <https://tibi-svelte-starter.code.testversion.online/> |
|
||||||
| Tibi Admin | <https://tibi-svelte-starter-tibiadmin.code.testversion.online/> |
|
| Tibi Admin | <https://tibi-svelte-starter-tibiadmin.code.testversion.online/> |
|
||||||
|
|||||||
@@ -164,6 +164,12 @@ const { ssrRequest } = require("../lib/ssr-server")
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tpl = context.fs.readFile("templates/spa.html")
|
var tpl = context.fs.readFile("templates/spa.html")
|
||||||
|
|
||||||
|
// Extract language from URL for <html lang="..."> (before other replacements)
|
||||||
|
var langMatch = url.match(/^\/(de|en)(\/|$)/)
|
||||||
|
var pageLang = langMatch ? langMatch[1] : "de"
|
||||||
|
tpl = tpl.replace(/<html lang="[^"]*">/, '<html lang="' + pageLang + '">')
|
||||||
|
|
||||||
tpl = tpl.replace("<!--HEAD-->", head)
|
tpl = tpl.replace("<!--HEAD-->", head)
|
||||||
tpl = tpl.replace("<!--HTML-->", html)
|
tpl = tpl.replace("<!--HTML-->", html)
|
||||||
tpl = tpl.replace("<!--SSR.ERROR-->", error ? "<!--" + error + "-->" : "")
|
tpl = tpl.replace("<!--SSR.ERROR-->", error ? "<!--" + error + "-->" : "")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
frontend/spa.html
|
../../frontend/spa.html
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<title>__PROJECT_TITLE__</title>
|
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="stylesheet" href="/dist/index.css?t=__TIMESTAMP__" />
|
<link rel="stylesheet" href="/dist/index.css?t=__TIMESTAMP__" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { untrack } from "svelte"
|
||||||
import { metricCall } from "./config"
|
import { metricCall } from "./config"
|
||||||
import { location, mobileMenuOpen, currentContentEntry } from "./lib/store"
|
import { location, mobileMenuOpen, currentContentEntry } from "./lib/store"
|
||||||
import { _, locale } from "./lib/i18n/index"
|
import { _, locale } from "./lib/i18n/index"
|
||||||
@@ -21,23 +22,25 @@
|
|||||||
stripLanguageFromPath,
|
stripLanguageFromPath,
|
||||||
} from "./lib/i18n"
|
} from "./lib/i18n"
|
||||||
|
|
||||||
export let url = ""
|
let { url = "" }: { url?: string } = $props()
|
||||||
|
|
||||||
initScrollRestoration()
|
initScrollRestoration()
|
||||||
|
|
||||||
if (url) {
|
// SSR: capture initial URL once (not reactive — url is only set server-side)
|
||||||
// ssr
|
untrack(() => {
|
||||||
let l = url.split("?")
|
if (url) {
|
||||||
$location = {
|
let l = url.split("?")
|
||||||
path: l[0],
|
$location = {
|
||||||
search: l.length > 1 ? l[1] : "",
|
path: l[0],
|
||||||
hash: "",
|
search: l.length > 1 ? l[1] : "",
|
||||||
push: false,
|
hash: "",
|
||||||
pop: false,
|
push: false,
|
||||||
|
pop: false,
|
||||||
|
}
|
||||||
|
const lang = extractLanguageFromPath(l[0]) || DEFAULT_LANGUAGE
|
||||||
|
$locale = lang
|
||||||
}
|
}
|
||||||
const lang = extractLanguageFromPath(l[0]) || DEFAULT_LANGUAGE
|
})
|
||||||
$locale = lang
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect root "/" to default language
|
// Redirect root "/" to default language
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -137,8 +140,25 @@
|
|||||||
const routePath = stripLanguageFromPath($location.path)
|
const routePath = stripLanguageFromPath($location.path)
|
||||||
loadContent(lang, routePath || "/")
|
loadContent(lang, routePath || "/")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Dynamic page title
|
||||||
|
const SITE_NAME = "Tibi Starter"
|
||||||
|
let pageTitle = $derived(
|
||||||
|
contentEntry?.meta?.title
|
||||||
|
? `${contentEntry.meta.title} — ${SITE_NAME}`
|
||||||
|
: contentEntry?.name
|
||||||
|
? `${contentEntry.name} — ${SITE_NAME}`
|
||||||
|
: SITE_NAME
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
{#if contentEntry?.meta?.description}
|
||||||
|
<meta name="description" content={contentEntry.meta.description} />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<LoadingBar />
|
<LoadingBar />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
|
||||||
|
|||||||
@@ -65,9 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decorative bottom wave -->
|
<!-- Decorative bottom wave -->
|
||||||
<div class="absolute bottom-0 left-0 right-0 z-10">
|
<div class="absolute -bottom-px left-0 right-0 z-10">
|
||||||
<svg viewBox="0 0 1440 80" fill="none" class="w-full h-auto">
|
<svg viewBox="0 0 1440 80" preserveAspectRatio="none" class="block w-full h-auto">
|
||||||
<path d="M0 80V40C240 10 480 0 720 20C960 40 1200 50 1440 30V80H0Z" fill="white"></path>
|
<path d="M0 80V40C240 10 480 0 720 20C960 40 1200 50 1440 30V80H0Z" fill="currentColor" class="text-white"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -20,17 +20,17 @@
|
|||||||
|
|
||||||
/* ── Custom theme tokens ─────────────────────────────────────────── */
|
/* ── Custom theme tokens ─────────────────────────────────────────── */
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-50: #f0f5ff;
|
--color-brand-50: #fef2f3;
|
||||||
--color-brand-100: #e0eaff;
|
--color-brand-100: #fee2e5;
|
||||||
--color-brand-200: #c7d7fe;
|
--color-brand-200: #fecacd;
|
||||||
--color-brand-300: #a4bcfd;
|
--color-brand-300: #fda4ab;
|
||||||
--color-brand-400: #8098f9;
|
--color-brand-400: #fb6e7c;
|
||||||
--color-brand-500: #6172f3;
|
--color-brand-500: #f14152;
|
||||||
--color-brand-600: #444ce7;
|
--color-brand-600: #ce2237;
|
||||||
--color-brand-700: #3538cd;
|
--color-brand-700: #ac1526;
|
||||||
--color-brand-800: #2d31a6;
|
--color-brand-800: #8f1523;
|
||||||
--color-brand-900: #2d3282;
|
--color-brand-900: #7a1723;
|
||||||
--color-brand-950: #1f235b;
|
--color-brand-950: #43070f;
|
||||||
|
|
||||||
--color-accent-400: #f79009;
|
--color-accent-400: #f79009;
|
||||||
--color-accent-500: #f59e0b;
|
--color-accent-500: #f59e0b;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { addMessages, init } from "svelte-i18n"
|
import { render } from "svelte/server"
|
||||||
import { DEFAULT_LANGUAGE } from "./lib/i18n"
|
import { addMessages, init, locale } from "svelte-i18n"
|
||||||
|
import { DEFAULT_LANGUAGE, extractLanguageFromPath } from "./lib/i18n"
|
||||||
import deLocale from "./lib/i18n/locales/de.json"
|
import deLocale from "./lib/i18n/locales/de.json"
|
||||||
import enLocale from "./lib/i18n/locales/en.json"
|
import enLocale from "./lib/i18n/locales/en.json"
|
||||||
import App from "./App.svelte"
|
import App from "./App.svelte"
|
||||||
@@ -8,9 +9,30 @@ import App from "./App.svelte"
|
|||||||
addMessages("de", deLocale)
|
addMessages("de", deLocale)
|
||||||
addMessages("en", enLocale)
|
addMessages("en", enLocale)
|
||||||
|
|
||||||
init({
|
/**
|
||||||
fallbackLocale: DEFAULT_LANGUAGE,
|
* SSR render wrapper for Svelte 5.
|
||||||
initialLocale: DEFAULT_LANGUAGE,
|
*
|
||||||
})
|
* tibi-server calls `app.default.render({ url })`.
|
||||||
|
* Svelte 5 no longer provides a `.render()` method on components —
|
||||||
|
* instead `render()` must be imported from `svelte/server`.
|
||||||
|
*
|
||||||
|
* This wrapper keeps the hook-side API compatible while delegating
|
||||||
|
* to the Svelte 5 render function internally.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
render({ url }: { url: string }) {
|
||||||
|
const lang = extractLanguageFromPath(url) || DEFAULT_LANGUAGE
|
||||||
|
|
||||||
export default App
|
init({
|
||||||
|
fallbackLocale: DEFAULT_LANGUAGE,
|
||||||
|
initialLocale: lang,
|
||||||
|
})
|
||||||
|
locale.set(lang)
|
||||||
|
|
||||||
|
const { body, head } = render(App, {
|
||||||
|
props: { url },
|
||||||
|
})
|
||||||
|
|
||||||
|
return { html: body, head }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user