feat: add sitemap.xml endpoint and improve SEO metadata across web application. Update landing page and root route with Open Graph and Twitter Card tags, enhance environment configuration for site URL, and introduce robots.txt for better search engine indexing. Include Umami analytics script for tracking user interactions.
Browse files- apps/server/src/index.ts +20 -0
- apps/web/.env.example +1 -1
- apps/web/index.html +5 -1
- apps/web/public/og_image.png +3 -0
- apps/web/public/robots.txt +3 -0
- apps/web/src/routes/__root.tsx +45 -11
- apps/web/src/routes/landing.tsx +12 -0
- apps/web/vite.config.ts +10 -0
- packages/env/src/web.ts +1 -0
apps/server/src/index.ts
CHANGED
|
@@ -56,6 +56,26 @@ app.use(
|
|
| 56 |
}),
|
| 57 |
);
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
app.get("/", (c) => {
|
| 60 |
return c.text("OK");
|
| 61 |
});
|
|
|
|
| 56 |
}),
|
| 57 |
);
|
| 58 |
|
| 59 |
+
app.get("/sitemap.xml", (c) => {
|
| 60 |
+
const baseUrl = "https://labas.rogasper.com";
|
| 61 |
+
const urls = [
|
| 62 |
+
{ loc: `${baseUrl}/landing`, changefreq: "weekly", priority: "0.8" },
|
| 63 |
+
];
|
| 64 |
+
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
| 65 |
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
| 66 |
+
${urls
|
| 67 |
+
.map(
|
| 68 |
+
(url) => ` <url>
|
| 69 |
+
<loc>${url.loc}</loc>
|
| 70 |
+
<changefreq>${url.changefreq}</changefreq>
|
| 71 |
+
<priority>${url.priority}</priority>
|
| 72 |
+
</url>`,
|
| 73 |
+
)
|
| 74 |
+
.join("\n")}
|
| 75 |
+
</urlset>`;
|
| 76 |
+
return c.text(sitemap, 200, { "Content-Type": "application/xml" });
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
app.get("/", (c) => {
|
| 80 |
return c.text("OK");
|
| 81 |
});
|
apps/web/.env.example
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
VITE_SERVER_URL=http://localhost:3000
|
| 2 |
-
|
| 3 |
|
|
|
|
| 1 |
VITE_SERVER_URL=http://localhost:3000
|
| 2 |
+
VITE_SITE_URL=http://localhost:5173
|
| 3 |
|
apps/web/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html lang="
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -7,10 +7,14 @@
|
|
| 7 |
<meta name="theme-color" content="#0c0c0c" />
|
| 8 |
<meta property="og:title" content="Labas β AI Exam Practice" />
|
| 9 |
<meta property="og:description" content="AI-powered multi-language test practice platform" />
|
|
|
|
|
|
|
| 10 |
<meta property="og:type" content="website" />
|
| 11 |
<meta name="twitter:card" content="summary_large_image" />
|
| 12 |
<meta name="twitter:title" content="Labas β AI Exam Practice" />
|
| 13 |
<meta name="twitter:description" content="AI-powered multi-language test practice platform" />
|
|
|
|
|
|
|
| 14 |
<title>labas</title>
|
| 15 |
</head>
|
| 16 |
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="id">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
| 7 |
<meta name="theme-color" content="#0c0c0c" />
|
| 8 |
<meta property="og:title" content="Labas β AI Exam Practice" />
|
| 9 |
<meta property="og:description" content="AI-powered multi-language test practice platform" />
|
| 10 |
+
<meta property="og:url" content="https://labas.rogasper.com" />
|
| 11 |
+
<meta property="og:image" content="https://labas.rogasper.com/og_image.png" />
|
| 12 |
<meta property="og:type" content="website" />
|
| 13 |
<meta name="twitter:card" content="summary_large_image" />
|
| 14 |
<meta name="twitter:title" content="Labas β AI Exam Practice" />
|
| 15 |
<meta name="twitter:description" content="AI-powered multi-language test practice platform" />
|
| 16 |
+
<meta name="twitter:image" content="https://labas.rogasper.com/og_image.png" />
|
| 17 |
+
<link rel="canonical" href="https://labas.rogasper.com" />
|
| 18 |
<title>labas</title>
|
| 19 |
</head>
|
| 20 |
|
apps/web/public/og_image.png
ADDED
|
Git LFS Details
|
apps/web/public/robots.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
User-agent: *
|
| 2 |
+
Allow: /
|
| 3 |
+
Sitemap: https://labas.rogasper.com/sitemap.xml
|
apps/web/src/routes/__root.tsx
CHANGED
|
@@ -32,18 +32,52 @@ export interface RouterAppContext {
|
|
| 32 |
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
| 33 |
component: RootComponent,
|
| 34 |
errorComponent: ({ error, reset }) => <ErrorFallback error={error} reset={reset} />,
|
| 35 |
-
head: () =>
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
links: [
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
});
|
| 48 |
|
| 49 |
function RootComponent() {
|
|
|
|
| 32 |
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
| 33 |
component: RootComponent,
|
| 34 |
errorComponent: ({ error, reset }) => <ErrorFallback error={error} reset={reset} />,
|
| 35 |
+
head: () => {
|
| 36 |
+
const siteUrl = "https://labas.rogasper.com";
|
| 37 |
+
return {
|
| 38 |
+
meta: [
|
| 39 |
+
{ title: "Labas β AI Exam Practice" },
|
| 40 |
+
{ name: "description", content: "AI-powered multi-language test practice platform" },
|
| 41 |
+
{ property: "og:title", content: "Labas β AI Exam Practice" },
|
| 42 |
+
{ property: "og:description", content: "AI-powered multi-language test practice platform" },
|
| 43 |
+
{ property: "og:url", content: siteUrl },
|
| 44 |
+
{ property: "og:image", content: `${siteUrl}/og_image.png` },
|
| 45 |
+
{ property: "og:type", content: "website" },
|
| 46 |
+
{ name: "twitter:card", content: "summary_large_image" },
|
| 47 |
+
{ name: "twitter:title", content: "Labas β AI Exam Practice" },
|
| 48 |
+
{ name: "twitter:description", content: "AI-powered multi-language test practice platform" },
|
| 49 |
+
{ name: "twitter:image", content: `${siteUrl}/og_image.png` },
|
| 50 |
+
{ name: "robots", content: "index, follow" },
|
| 51 |
+
],
|
| 52 |
links: [
|
| 53 |
+
{ rel: "icon", href: "/labas_icon.png" },
|
| 54 |
+
{ rel: "canonical", href: siteUrl },
|
| 55 |
+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
| 56 |
+
{ rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" as const },
|
| 57 |
+
{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Inter:wght@400;500;600&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&display=swap" },
|
| 58 |
+
],
|
| 59 |
+
scripts: [
|
| 60 |
+
...(import.meta.env.PROD
|
| 61 |
+
? [{ src: "https://umami-analytic.rogasper.com/script.js", defer: true, "data-website-id": "67a18412-12c8-44ef-9cd3-e04238d37e9a" }]
|
| 62 |
+
: []),
|
| 63 |
+
{
|
| 64 |
+
type: "application/ld+json",
|
| 65 |
+
children: JSON.stringify({
|
| 66 |
+
"@context": "https://schema.org",
|
| 67 |
+
"@type": "WebSite",
|
| 68 |
+
name: "Labas",
|
| 69 |
+
url: siteUrl,
|
| 70 |
+
description: "AI-powered multi-language test practice platform",
|
| 71 |
+
potentialAction: {
|
| 72 |
+
"@type": "SearchAction",
|
| 73 |
+
target: `${siteUrl}/bank?search={search_term_string}`,
|
| 74 |
+
"query-input": "required name=search_term_string",
|
| 75 |
+
},
|
| 76 |
+
}),
|
| 77 |
+
},
|
| 78 |
+
],
|
| 79 |
+
};
|
| 80 |
+
},
|
| 81 |
});
|
| 82 |
|
| 83 |
function RootComponent() {
|
apps/web/src/routes/landing.tsx
CHANGED
|
@@ -2,6 +2,18 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
| 2 |
import { authClient } from "@/lib/auth-client";
|
| 3 |
|
| 4 |
export const Route = createFileRoute("/landing")({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
beforeLoad: async () => {
|
| 6 |
try {
|
| 7 |
const session = await authClient.getSession();
|
|
|
|
| 2 |
import { authClient } from "@/lib/auth-client";
|
| 3 |
|
| 4 |
export const Route = createFileRoute("/landing")({
|
| 5 |
+
head: () => ({
|
| 6 |
+
meta: [
|
| 7 |
+
{ title: "Labas β AI-Powered Exam Practice Platform" },
|
| 8 |
+
{ name: "description", content: "Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." },
|
| 9 |
+
{ property: "og:title", content: "Labas β AI-Powered Exam Practice Platform" },
|
| 10 |
+
{ property: "og:description", content: "Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." },
|
| 11 |
+
{ property: "og:url", content: "https://labas.rogasper.com/landing" },
|
| 12 |
+
],
|
| 13 |
+
links: [
|
| 14 |
+
{ rel: "canonical", href: "https://labas.rogasper.com/landing" },
|
| 15 |
+
],
|
| 16 |
+
}),
|
| 17 |
beforeLoad: async () => {
|
| 18 |
try {
|
| 19 |
const session = await authClient.getSession();
|
apps/web/vite.config.ts
CHANGED
|
@@ -24,6 +24,16 @@ export default defineConfig({
|
|
| 24 |
},
|
| 25 |
},
|
| 26 |
plugins: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
tailwindcss(),
|
| 28 |
tanstackRouter({
|
| 29 |
target: "react",
|
|
|
|
| 24 |
},
|
| 25 |
},
|
| 26 |
plugins: [
|
| 27 |
+
{
|
| 28 |
+
name: "inject-umami",
|
| 29 |
+
transformIndexHtml(html) {
|
| 30 |
+
return html.replace(
|
| 31 |
+
"</head>",
|
| 32 |
+
'<script defer src="https://umami-analytic.rogasper.com/script.js" data-website-id="67a18412-12c8-44ef-9cd3-e04238d37e9a"></script></head>',
|
| 33 |
+
);
|
| 34 |
+
},
|
| 35 |
+
apply: "build",
|
| 36 |
+
},
|
| 37 |
tailwindcss(),
|
| 38 |
tanstackRouter({
|
| 39 |
target: "react",
|
packages/env/src/web.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const env = createEnv({
|
|
| 5 |
clientPrefix: "VITE_",
|
| 6 |
client: {
|
| 7 |
VITE_SERVER_URL: z.url(),
|
|
|
|
| 8 |
},
|
| 9 |
runtimeEnv: (import.meta as any).env,
|
| 10 |
emptyStringAsUndefined: true,
|
|
|
|
| 5 |
clientPrefix: "VITE_",
|
| 6 |
client: {
|
| 7 |
VITE_SERVER_URL: z.url(),
|
| 8 |
+
VITE_SITE_URL: z.url().default("http://localhost:5173"),
|
| 9 |
},
|
| 10 |
runtimeEnv: (import.meta as any).env,
|
| 11 |
emptyStringAsUndefined: true,
|