feat: update Open Graph and Twitter Card metadata in index.html, introduce new image assets for social previews, and enhance image compression script for better compatibility. Maintain legacy image path for backward compatibility.
Browse files- apps/web/index.html +8 -3
- apps/web/public/og_image.png +2 -2
- apps/web/public/opengraph-image.png +3 -0
- apps/web/public/twitter-image.jpg +3 -0
- apps/web/src/lib/site-seo.ts +14 -2
- scripts/compress-og-image.ts +39 -24
apps/web/index.html
CHANGED
|
@@ -8,9 +8,11 @@
|
|
| 8 |
<meta property="og:title" content="Labas — AI-Powered Exam Practice Platform" />
|
| 9 |
<meta property="og:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
|
| 10 |
<meta property="og:url" content="https://labas.rogasper.com/" />
|
|
|
|
|
|
|
| 11 |
<meta property="og:type" content="website" />
|
| 12 |
-
<meta property="og:image" content="https://labas.rogasper.com/
|
| 13 |
-
<meta property="og:image:secure_url" content="https://labas.rogasper.com/
|
| 14 |
<meta property="og:image:type" content="image/png" />
|
| 15 |
<meta property="og:image:width" content="1200" />
|
| 16 |
<meta property="og:image:height" content="630" />
|
|
@@ -18,8 +20,11 @@
|
|
| 18 |
<meta name="twitter:card" content="summary_large_image" />
|
| 19 |
<meta name="twitter:title" content="Labas — AI-Powered Exam Practice Platform" />
|
| 20 |
<meta name="twitter:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
|
| 21 |
-
<meta name="twitter:image" content="https://labas.rogasper.com/
|
| 22 |
<meta name="twitter:image:alt" content="Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
|
|
|
|
|
|
|
|
|
|
| 23 |
<meta name="robots" content="index, follow" />
|
| 24 |
<link rel="canonical" href="https://labas.rogasper.com/" />
|
| 25 |
<title>Labas — AI-Powered Exam Practice Platform</title>
|
|
|
|
| 8 |
<meta property="og:title" content="Labas — AI-Powered Exam Practice Platform" />
|
| 9 |
<meta property="og:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
|
| 10 |
<meta property="og:url" content="https://labas.rogasper.com/" />
|
| 11 |
+
<meta property="og:site_name" content="Labas" />
|
| 12 |
+
<meta property="og:locale" content="id_ID" />
|
| 13 |
<meta property="og:type" content="website" />
|
| 14 |
+
<meta property="og:image" content="https://labas.rogasper.com/opengraph-image.png" />
|
| 15 |
+
<meta property="og:image:secure_url" content="https://labas.rogasper.com/opengraph-image.png" />
|
| 16 |
<meta property="og:image:type" content="image/png" />
|
| 17 |
<meta property="og:image:width" content="1200" />
|
| 18 |
<meta property="og:image:height" content="630" />
|
|
|
|
| 20 |
<meta name="twitter:card" content="summary_large_image" />
|
| 21 |
<meta name="twitter:title" content="Labas — AI-Powered Exam Practice Platform" />
|
| 22 |
<meta name="twitter:description" content="Generate AI-powered practice questions for JLPT, TOPIK, TOAFL, and more. Practice smarter with adaptive test preparation." />
|
| 23 |
+
<meta name="twitter:image" content="https://labas.rogasper.com/twitter-image.jpg" />
|
| 24 |
<meta name="twitter:image:alt" content="Labas — platform latihan ujian bahasa dengan AI Generator dan simulasi tes" />
|
| 25 |
+
<meta name="twitter:image:type" content="image/jpeg" />
|
| 26 |
+
<meta name="twitter:image:width" content="1200" />
|
| 27 |
+
<meta name="twitter:image:height" content="630" />
|
| 28 |
<meta name="robots" content="index, follow" />
|
| 29 |
<link rel="canonical" href="https://labas.rogasper.com/" />
|
| 30 |
<title>Labas — AI-Powered Exam Practice Platform</title>
|
apps/web/public/og_image.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
apps/web/public/opengraph-image.png
ADDED
|
Git LFS Details
|
apps/web/public/twitter-image.jpg
ADDED
|
Git LFS Details
|
apps/web/src/lib/site-seo.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
| 1 |
export const SITE_URL = "https://labas.rogasper.com";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
export const OG_IMAGE_PATH = "/og_image.png";
|
| 4 |
export const OG_IMAGE_URL = `${SITE_URL}${OG_IMAGE_PATH}`;
|
|
|
|
| 5 |
export const OG_IMAGE_WIDTH = 1200;
|
| 6 |
export const OG_IMAGE_HEIGHT = 630;
|
| 7 |
export const OG_IMAGE_ALT =
|
|
@@ -28,6 +35,8 @@ export function buildSocialMeta(input: SiteHeadInput = {}) {
|
|
| 28 |
{ property: "og:title", content: title },
|
| 29 |
{ property: "og:description", content: description },
|
| 30 |
{ property: "og:url", content: url },
|
|
|
|
|
|
|
| 31 |
{ property: "og:type", content: "website" },
|
| 32 |
{ property: "og:image", content: OG_IMAGE_URL },
|
| 33 |
{ property: "og:image:secure_url", content: OG_IMAGE_URL },
|
|
@@ -38,7 +47,10 @@ export function buildSocialMeta(input: SiteHeadInput = {}) {
|
|
| 38 |
{ name: "twitter:card", content: "summary_large_image" },
|
| 39 |
{ name: "twitter:title", content: title },
|
| 40 |
{ name: "twitter:description", content: description },
|
| 41 |
-
{ name: "twitter:image", content:
|
| 42 |
{ name: "twitter:image:alt", content: OG_IMAGE_ALT },
|
|
|
|
|
|
|
|
|
|
| 43 |
];
|
| 44 |
}
|
|
|
|
| 1 |
export const SITE_URL = "https://labas.rogasper.com";
|
| 2 |
+
export const SITE_NAME = "Labas";
|
| 3 |
+
|
| 4 |
+
/** Match rogasper.com: separate OG (PNG) + Twitter (JPEG) assets for crawler compatibility. */
|
| 5 |
+
export const OG_IMAGE_PATH = "/opengraph-image.png";
|
| 6 |
+
export const TWITTER_IMAGE_PATH = "/twitter-image.jpg";
|
| 7 |
+
/** @deprecated Use OG_IMAGE_PATH — kept for old links */
|
| 8 |
+
export const LEGACY_OG_IMAGE_PATH = "/og_image.png";
|
| 9 |
|
|
|
|
| 10 |
export const OG_IMAGE_URL = `${SITE_URL}${OG_IMAGE_PATH}`;
|
| 11 |
+
export const TWITTER_IMAGE_URL = `${SITE_URL}${TWITTER_IMAGE_PATH}`;
|
| 12 |
export const OG_IMAGE_WIDTH = 1200;
|
| 13 |
export const OG_IMAGE_HEIGHT = 630;
|
| 14 |
export const OG_IMAGE_ALT =
|
|
|
|
| 35 |
{ property: "og:title", content: title },
|
| 36 |
{ property: "og:description", content: description },
|
| 37 |
{ property: "og:url", content: url },
|
| 38 |
+
{ property: "og:site_name", content: SITE_NAME },
|
| 39 |
+
{ property: "og:locale", content: "id_ID" },
|
| 40 |
{ property: "og:type", content: "website" },
|
| 41 |
{ property: "og:image", content: OG_IMAGE_URL },
|
| 42 |
{ property: "og:image:secure_url", content: OG_IMAGE_URL },
|
|
|
|
| 47 |
{ name: "twitter:card", content: "summary_large_image" },
|
| 48 |
{ name: "twitter:title", content: title },
|
| 49 |
{ name: "twitter:description", content: description },
|
| 50 |
+
{ name: "twitter:image", content: TWITTER_IMAGE_URL },
|
| 51 |
{ name: "twitter:image:alt", content: OG_IMAGE_ALT },
|
| 52 |
+
{ name: "twitter:image:type", content: "image/jpeg" },
|
| 53 |
+
{ name: "twitter:image:width", content: String(OG_IMAGE_WIDTH) },
|
| 54 |
+
{ name: "twitter:image:height", content: String(OG_IMAGE_HEIGHT) },
|
| 55 |
];
|
| 56 |
}
|
scripts/compress-og-image.ts
CHANGED
|
@@ -1,19 +1,24 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
* Requires Bun >= 1.3.14 (Bun.Image API).
|
| 4 |
*
|
|
|
|
|
|
|
| 5 |
* Usage: bun scripts/compress-og-image.ts
|
| 6 |
*/
|
| 7 |
|
| 8 |
import { join } from "node:path";
|
| 9 |
|
| 10 |
const ROOT = join(import.meta.dir, "..");
|
| 11 |
-
const
|
|
|
|
|
|
|
|
|
|
| 12 |
const WIDTH = 1200;
|
| 13 |
const HEIGHT = 630;
|
| 14 |
|
| 15 |
-
function requireBunImage() {
|
| 16 |
-
const file = Bun.file(
|
| 17 |
if (typeof file.image !== "function") {
|
| 18 |
console.error(
|
| 19 |
"Bun.Image is not available. Upgrade Bun to >= 1.3.14:\n" +
|
|
@@ -29,27 +34,37 @@ function formatKb(bytes: number) {
|
|
| 29 |
return `${(bytes / 1024).toFixed(1)} KB`;
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
async function main() {
|
| 33 |
-
const before = (await Bun.file(
|
| 34 |
-
console.log(`Source: ${
|
| 35 |
-
console.log(`Before: ${formatKb(before)}
|
| 36 |
-
|
| 37 |
-
const
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
await
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
main().catch((err) => {
|
|
|
|
| 1 |
/**
|
| 2 |
+
* Build social preview images for Labas (Open Graph + Twitter).
|
| 3 |
* Requires Bun >= 1.3.14 (Bun.Image API).
|
| 4 |
*
|
| 5 |
+
* Outputs truecolor PNG + JPEG — palette PNG breaks some Twitter/Threads crawlers.
|
| 6 |
+
*
|
| 7 |
* Usage: bun scripts/compress-og-image.ts
|
| 8 |
*/
|
| 9 |
|
| 10 |
import { join } from "node:path";
|
| 11 |
|
| 12 |
const ROOT = join(import.meta.dir, "..");
|
| 13 |
+
const PUBLIC = join(ROOT, "apps/web/public");
|
| 14 |
+
const SOURCE = join(PUBLIC, "og_image.png");
|
| 15 |
+
const OPENGRAPH = join(PUBLIC, "opengraph-image.png");
|
| 16 |
+
const TWITTER = join(PUBLIC, "twitter-image.jpg");
|
| 17 |
const WIDTH = 1200;
|
| 18 |
const HEIGHT = 630;
|
| 19 |
|
| 20 |
+
function requireBunImage(path: string) {
|
| 21 |
+
const file = Bun.file(path);
|
| 22 |
if (typeof file.image !== "function") {
|
| 23 |
console.error(
|
| 24 |
"Bun.Image is not available. Upgrade Bun to >= 1.3.14:\n" +
|
|
|
|
| 34 |
return `${(bytes / 1024).toFixed(1)} KB`;
|
| 35 |
}
|
| 36 |
|
| 37 |
+
async function writeMeta(label: string, path: string) {
|
| 38 |
+
const bytes = (await Bun.file(path).arrayBuffer()).byteLength;
|
| 39 |
+
const meta = await Bun.file(path).image().metadata();
|
| 40 |
+
console.log(` ${label}: ${formatKb(bytes)} (${meta.width}×${meta.height}, ${meta.format})`);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
async function main() {
|
| 44 |
+
const before = (await Bun.file(SOURCE).arrayBuffer()).byteLength;
|
| 45 |
+
console.log(`Source: ${SOURCE}`);
|
| 46 |
+
console.log(`Before: ${formatKb(before)}`);
|
| 47 |
+
|
| 48 |
+
const base = requireBunImage(SOURCE).resize(WIDTH, HEIGHT, {
|
| 49 |
+
fit: "fill",
|
| 50 |
+
filter: "lanczos3",
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
// Truecolor PNG — compatible with Meta/Twitter crawlers (no 8-bit palette).
|
| 54 |
+
await base.png({ compressionLevel: 9, palette: false }).write(OPENGRAPH);
|
| 55 |
+
|
| 56 |
+
// JPEG for twitter:image — matches rogasper.com pattern, very reliable on X/Threads.
|
| 57 |
+
await requireBunImage(OPENGRAPH)
|
| 58 |
+
.jpeg({ quality: 88, mozjpeg: true })
|
| 59 |
+
.write(TWITTER);
|
| 60 |
+
|
| 61 |
+
// Keep legacy filename in sync with the Open Graph asset.
|
| 62 |
+
await Bun.write(SOURCE, Bun.file(OPENGRAPH));
|
| 63 |
+
|
| 64 |
+
console.log("Wrote:");
|
| 65 |
+
await writeMeta("opengraph-image.png", OPENGRAPH);
|
| 66 |
+
await writeMeta("twitter-image.jpg", TWITTER);
|
| 67 |
+
await writeMeta("og_image.png (legacy)", SOURCE);
|
| 68 |
}
|
| 69 |
|
| 70 |
main().catch((err) => {
|