feat: wip i18n, new branding

This commit is contained in:
taskylizard 2024-02-10 14:44:41 +00:00
parent 5edea88ad0
commit bfd41231ad
33 changed files with 2700 additions and 318 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string }>();
</script>
<template>
<div tw="w-full h-full bg-black flex flex-col" style="
background-image: linear-gradient(
45deg,
rgb(14, 14, 25) 0%,
rgb(29, 33, 43) 19%,
rgb(47, 53, 63) 27%,
rgb(71, 84, 97) 34%,
rgb(101, 122, 133) 41%,
rgb(136, 163, 170) 47%,
rgb(167, 188, 185) 53%,
rgb(192, 203, 191) 59%,
rgb(216, 214, 199) 66%,
rgb(236, 223, 202) 73%,
rgb(248, 231, 203) 81%,
rgb(255, 238, 204) 100%
);
">
<div tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between">
<div tw="w-full flex justify-between items-center text-5xl font-medium">
<div tw="flex items-center">
<div tw="text-zinc-100 ml-2 mt-1 font-semibold">
the privateersclub wiki
</div>
</div>
</div>
<div tw="w-full pr-56 flex flex-col items-start justify-end">
<div style="color: #eeeeee" tw="text-6xl font-bold" v-html="title" />
<div style="color: #adf0dd; text-decoration-style: dotted" tw="mt-2 text-4xl underline" v-html="description" />
</div>
</div>
<div tw="shrink-0 h-4 w-full flex" style="background-color: #adf0dd" />
</div>
</template>

View File

@ -0,0 +1,100 @@
import type { HeadConfig, TransformContext } from "vitepress";
export function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = [];
const { pageData } = context;
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`;
head.push(
["link", { rel: "canonical", href: url }],
["meta", { property: "og:url", content: url }],
["meta", { name: "twitter:url", content: url }],
["meta", { name: "twitter:card", content: "summary_large_image" }],
["meta", { property: "og:title", content: pageData.frontmatter.title }],
["meta", { name: "twitter:title", content: pageData.frontmatter.title }],
);
if (pageData.frontmatter.description) {
head.push(
[
"meta",
{
property: "og:description",
content: pageData.frontmatter.description,
},
],
[
"meta",
{
name: "twitter:description",
content: pageData.frontmatter.description,
},
],
);
}
if (pageData.frontmatter.image) {
head.push([
"meta",
{
property: "og:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
]);
head.push([
"meta",
{
name: "twitter:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
]);
} else {
const url = pageData.filePath.replace("index.md", "").replace(".md", "");
const imageUrl = `${url}/__og_image__/og.png`
.replaceAll("//", "/")
.replace(/^\//, "");
head.push(
["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }],
["meta", { property: "og:image:width", content: "1200" }],
["meta", { property: "og:image:height", content: "628" }],
["meta", { property: "og:image:type", content: "image/png" }],
[
"meta",
{ property: "og:image:alt", content: pageData.frontmatter.title },
],
["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }],
["meta", { name: "twitter:image:width", content: "1200" }],
["meta", { name: "twitter:image:height", content: "628" }],
[
"meta",
{ name: "twitter:image:alt", content: pageData.frontmatter.title },
],
);
}
if (pageData.frontmatter.tag) {
head.push([
"meta",
{ property: "article:tag", content: pageData.frontmatter.tag },
]);
}
if (pageData.frontmatter.date) {
head.push([
"meta",
{
property: "article:published_time",
content: pageData.frontmatter.date,
},
]);
}
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([
"meta",
{
property: "article:modified_time",
content: new Date(pageData.lastUpdated).toISOString(),
},
]);
}
return head;
}

View File

@ -0,0 +1,98 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createContentLoader } from "vitepress";
import type { ContentData, SiteConfig } from "vitepress";
import { type SatoriOptions, satoriVue } from "x-satori/vue";
import { renderAsync } from "@resvg/resvg-js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const __fonts = resolve(__dirname, "../fonts");
export async function generateImages(config: SiteConfig): Promise<void> {
const pages = await createContentLoader("**/*.md", { excerpt: true }).load();
const template = await readFile(
resolve(__dirname, "./Template.vue"),
"utf-8",
);
const fonts: SatoriOptions["fonts"] = [
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
weight: 400,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
weight: 500,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
weight: 600,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
weight: 700,
style: "normal",
},
];
for (const page of pages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
});
}
return console.info("Generated opengraph images.");
}
interface GenerateImagesOptions {
page: ContentData;
template: string;
outDir: string;
fonts: SatoriOptions["fonts"];
}
async function generateImage({
page,
template,
outDir,
fonts,
}: GenerateImagesOptions): Promise<void> {
const { frontmatter, url } = page;
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title:
frontmatter.layout === "home"
? frontmatter.hero.name ?? frontmatter.title
: frontmatter.title,
description:
frontmatter.layout === "home"
? frontmatter.hero.tagline ?? frontmatter.description
: frontmatter.description,
},
};
const svg = await satoriVue(options, template);
const render = await renderAsync(svg);
const outputFolder = resolve(outDir, url.slice(1), "__og_image__");
const outputFile = resolve(outputFolder, "og.png");
await mkdir(outputFolder, { recursive: true });
await writeFile(outputFile, render.asPng());
}

View File

@ -0,0 +1,47 @@
import { readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { SatoriOptions } from "x-satori/vue";
import { defineSatoriConfig } from "x-satori/vue";
const __dirname = dirname(fileURLToPath(import.meta.url));
const __fonts = resolve(__dirname, "../fonts");
const fonts: SatoriOptions["fonts"] = [
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
weight: 400,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
weight: 500,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
weight: 600,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
weight: 700,
style: "normal",
},
];
export default defineSatoriConfig({
width: 1200,
height: 628,
fonts,
props: {
title: "Title",
description:
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.",
dir: "/j",
},
});

View File

@ -1,18 +1,25 @@
import { DefaultTheme, LocaleSpecificConfig } from "vitepress";
import { generateSidebar } from "../../sidebar";
const navbar: DefaultTheme.NavItem[] = [
{ text: "Get Started", link: "/start" },
{ text: "Contribute", link: "/contribute" },
];
const sidebar: DefaultTheme.Sidebar = [
{ text: "Glossary", link: "/glossary" },
{ text: "Software", link: "/software" },
{ text: "Download", link: "/download" },
{ text: "Emulation", link: "/emulation" },
{ text: "Useful", link: "/useful" },
{ text: "Unsafe", link: "/unsafe" },
];
export const enLocale: LocaleSpecificConfig<DefaultTheme.Config> = {
...navbar,
themeConfig: {
sidebar: generateSidebar("en-us"),
sidebar,
editLink: {
pattern:
"https://github.com/privateersclub/wiki/edit/master/docs/:path",
pattern: "https://github.com/privateersclub/wiki/edit/master/docs/:path",
text: "Suggest Changes",
},
},

View File

@ -1,7 +1,6 @@
// TODO: complete this translation, change only strings!
import { DefaultTheme, LocaleSpecificConfig } from "vitepress";
import { generateSidebar } from "../../sidebar";
const navbar: DefaultTheme.NavItem[] = [
{ text: "Get started", link: "/start" },
@ -11,7 +10,7 @@ const navbar: DefaultTheme.NavItem[] = [
export const brLocale: LocaleSpecificConfig<DefaultTheme.Config> = {
...navbar,
themeConfig: {
sidebar: generateSidebar("br"),
sidebar: [{}],
editLink: {
pattern: "https://github.com/privateersclub/wiki/edit/master/docs/:path",
text: "Suggest changes",

View File

@ -1,9 +1,11 @@
import { defineConfig } from "vitepress";
import UnoCSS from "unocss/vite";
import { generateImages } from "./hooks/opengraph";
import { generateMeta } from "./hooks/meta";
export const sharedConfig = defineConfig({
title: "privateersclub/wiki",
description:
"Welcome to the most comprehensive game piracy wiki on the internet.",
description: "The most comprehensive game piracy wiki on the internet.",
base: process.env.BASE_URL || "/",
lang: "en-US",
lastUpdated: true,
@ -11,20 +13,16 @@ export const sharedConfig = defineConfig({
appearance: "dark",
titleTemplate: ":title • Wiki",
head: [
['meta', { name: 'theme-color', content: '#7bc5e4' }],
['meta', { name: 'og:type', content: 'website' }],
['meta', { name: 'og:locale', content: 'en' }],
[
'script',
{
src: 'https://i-totally-love-easylist.swmg.top/vue.min.js',
'data-api': "https://i-totally-love-easylist.swmg.top/pong",
async: '',
'data-domain': 'megathread.pages.dev',
},
],
["meta", { name: "theme-color", content: "#ADF0DD" }],
["meta", { name: "og:type", content: "website" }],
["meta", { name: "og:locale", content: "en" }],
],
vite: { plugins: [UnoCSS()] },
transformHead: async (context) =>
generateMeta(context, "https://megathread.pages.dev"),
buildEnd(siteConfig) {
generateImages(siteConfig);
},
themeConfig: {
search: {
provider: "local",
@ -32,7 +30,12 @@ export const sharedConfig = defineConfig({
docFooter: { next: false, prev: false },
socialLinks: [
{ icon: "github", link: "https://github.com/privateersclub/wiki" },
{ icon: { svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16"><path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/></svg>' }, link: "https://privateer.divolt.xyz" }
{
icon: {
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16"><path d="M2.678 11.894a1 1 0 0 1 .287.801 10.97 10.97 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8.06 8.06 0 0 0 8 14c3.996 0 7-2.807 7-6 0-3.192-3.004-6-7-6S1 4.808 1 8c0 1.468.617 2.83 1.678 3.894zm-.493 3.905a21.682 21.682 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a9.68 9.68 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105z"/></svg>',
},
link: "https://privateer.divolt.xyz",
},
],
},
});

View File

@ -0,0 +1,23 @@
import nprogress, { type NProgress } from "nprogress";
import type { EnhanceAppContext } from "vitepress";
export function loadProgress(router: EnhanceAppContext["router"]): NProgress {
if (typeof window === "undefined") return;
setTimeout(() => {
nprogress.configure({ showSpinner: false });
const cacheBeforeRouteChange = router.onBeforeRouteChange;
const cacheAfterRouteChange = router.onAfterRouteChanged;
router.onBeforeRouteChange = (to) => {
nprogress.start();
cacheBeforeRouteChange?.(to);
};
router.onAfterRouteChanged = (to) => {
nprogress.done();
cacheAfterRouteChange?.(to);
};
});
return nprogress;
}

View File

@ -1,6 +1,12 @@
import Theme from "vitepress/theme";
import { type Theme } from "vitepress";
import DefaultTheme from "vitepress/theme";
import { loadProgress } from "./composables/nprogress";
import "./style.css";
import "uno.css";
export default {
...Theme,
};
extends: DefaultTheme,
enhanceApp({ router }) {
loadProgress(router);
},
} satisfies Theme;

View File

@ -1,9 +1,8 @@
:root {
/* Colors: Brand */
--vp-c-brand-1: #7bc5e4;
--vp-c-brand-2: #c4e2f2;
--vp-c-brand-3: #4882a7;
--vp-c-brand-soft: #a4d5ec;
--vp-c-brand-1: #1fd8a4;
--vp-c-brand-2: #4acdab;
--vp-c-brand-3: #3abd9d;
--vp-c-brand-soft: #2aae8f;
/* Colors: Button */
--vp-button-brand-bg: var(--vp-c-brand-1);
@ -92,3 +91,87 @@
::selection {
background-color: var(--vp-button-brand-bg);
}
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: var(--vp-c-brand-1);
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow:
0 0 10px var(--vp-c-brand-1),
0 0 5px var(--vp-c-brand-1);
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: var(--vp-c-brand);
border-left-color: var(--vp-c-brand);
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

5
docs/.vitepress/vue-shim.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/* eslint-disable ts/consistent-type-imports */
declare module "*.vue" {
const component: import("vue").Component;
export default component;
}

View File

@ -7,7 +7,7 @@ hero:
tagline: Welcome to the most comprehensive game piracy wiki on the internet.
actions:
- text: Get Started
link: /wiki/glossary
link: /glossary
- text: Contribute
link: /contribute

View File

@ -1,57 +0,0 @@
import fs from "fs";
import path from "path";
import matter, { type GrayMatterFile } from "gray-matter";
import { getTableOfContents, Items } from "../shared";
const dir = path.join(__dirname, "/wiki/");
async function getMetadata(
path: string,
): Promise<{ frontmatter: GrayMatterFile<string>; toc: Items }> {
const contents = fs.readFileSync(path, "utf8");
const frontmatter = matter(contents);
const toc = await getTableOfContents(contents);
return { frontmatter, toc };
}
export function generateSidebar(locale: string): any[] {
let localePath = "";
let localeUri = "";
const items: {
text: string;
link: string;
base: string;
items: { text: string; link: string }[];
}[] = [];
if (locale !== "en-us") {
localeUri = `/${locale}/wiki/`;
localePath = path.join(__dirname, localeUri);
} else {
// default directory for en-us
localeUri = "/wiki/";
localePath = dir;
}
fs.readdirSync(localePath).forEach(async (file) => {
const filePath = path.join(localePath, file);
const stat = fs.statSync(filePath);
if (stat.isFile() && path.extname(file) === ".md") {
const { frontmatter, toc } = await getMetadata(filePath);
const title = frontmatter.data.title;
if (!title) throw new Error("Cannot find title in metadata: " + filePath);
const tocItems = toc.items?.map((item) => ({
text: item.title,
link: `${file.replace(/\.md$/, "")}${item.url}`,
}));
items.push({
text: title,
link: `${file.replace(/\.md$/, "")}`,
base: `${localeUri}`,
items: tocItems!,
});
}
});
const sidebar = [{ items: items }];
return sidebar;
}

25
lunaria.config.json Normal file
View File

@ -0,0 +1,25 @@
{
"$schema": "./node_modules/@lunariajs/core/config.schema.json",
"repository": {
"name": "privateersclub/wiki",
"rootDir": "docs"
},
"files": [
{
"location": "**/*.md",
"pattern": "@lang/@path",
"type": "universal"
}
],
"defaultLocale": {
"label": "English",
"lang": "en"
},
"locales": [
{
"label": "Português",
"lang": "pt-br"
}
],
"outDir": ".vitepress/dist/_translations"
}

View File

@ -8,13 +8,17 @@
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:serve": "vitepress serve docs",
"docs:preview": "vitepress preview docs"
"docs:preview": "vitepress preview docs",
"lunaria:build": "lunaria build",
"lunaria:preview": "lunaria preview",
"og:dev": "x-satori -t ./docs/.vitepress/hooks/Template.vue -c ./docs/.vitepress/hooks/satoriConfig.ts --dev"
},
"license": "Unlicense",
"devDependencies": {
"@types/node": "^20.11.10",
"prettier": "^3.2.4",
"vitepress": "1.0.0-rc.40"
"@types/node": "^20.11.17",
"@types/nprogress": "^0.2.3",
"prettier": "^3.2.5",
"vitepress": "1.0.0-rc.42"
},
"pnpm": {
"peerDependencyRules": {
@ -24,11 +28,18 @@
}
},
"dependencies": {
"@lunariajs/core": "^0.0.28",
"@resvg/resvg-js": "^2.6.0",
"gray-matter": "^4.0.3",
"mdast-util-toc": "^7.0.0",
"nprogress": "^0.2.0",
"pathe": "^1.1.2",
"remark": "^15.0.1",
"remark-parse": "^11.0.0",
"unified": "^11.0.4",
"unist-util-visit": "^5.0.0"
"unist-util-visit": "^5.0.0",
"unocss": "^0.58.5",
"vue": "^3.4.18",
"x-satori": "^0.1.5"
}
}

File diff suppressed because it is too large Load Diff