Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
Expand All @@ -10,6 +11,7 @@ module.exports = {
"plugin:markdown/recommended",
"prettier",
],
parser: "vue-eslint-parser",
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ docs/.vitepress/cache
docs/.vitepress/dist

yarn-error.log

public/sitemap.xml
5 changes: 1 addition & 4 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

yarn lint-staged
npx lint-staged
10 changes: 6 additions & 4 deletions .lintstagedrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"*.{js,jsx,ts,tsx,md}": "eslint --fix",
"*": "prettier --write --ignore-unknown"
}
{
"*.{js,cjs,mjs,ts,vue,md,json}": [
"prettier --write",
"eslint --fix"
]
}
59 changes: 31 additions & 28 deletions docs/.vitepress/config/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,45 +30,48 @@ function sidebar(): DefaultTheme.SidebarItem[] {
text: "Introduction",
collapsed: false,
items: [
{ text: "Getting started", link: "/intro/beginning" },
{ text: "Flight styles", link: "/intro/flightstyles" },
{ text: "Simulators", link: "/intro/sim" },
{ text: "Transmitters", link: "/intro/tx" },
{ text: "Video systems", link: "/intro/vrx" },
{ text: "Chargers & Batteries", link: "/intro/power" },
{ text: "Getting started", link: "/en/intro/beginning" },
{ text: "Glossary", link: "/en/intro/glossarium" },
{ text: "Flight styles", link: "/en/intro/flightstyles" },
{ text: "Simulators", link: "/en/intro/sim" },
{ text: "Transmitters", link: "/en/intro/tx" },
{ text: "Video systems", link: "/en/intro/vrx" },
{ text: "Chargers & Batteries", link: "/en/intro/power" },
],
},
/* {
text: "Shopping",
collapsed: true,
items: [
{ text: "Must-have items", link: "/en/shop/nessesary" },
{ text: "For Airplane Pilots", link: "/en/shop/airplanes" },
{ text: "Starter Kits", link: "/en/shop/set" },
{ text: "DIY Components", link: "/en/shop/DIY" },
],
}, */
{
text: "Community",
collapsed: false,
collapsed: true,
items: [
{ text: "Dictionary", link: "https://oscarliang.com/fpv-dictionary/" },
// { text: "Chats", link: "/community/chats" },
// { text: "Useful Resources", link: "/community/usefull" },
{ text: "Software", link: "/community/software" },
{ text: "Soldering", link: "/community/soldering" },
// { text: "Waterproofing", link: "/community/coating" },
{ text: "Chats", link: "/en/community/chats" },
{ text: "Useful Resources", link: "/en/community/usefull" },
{ text: "Software", link: "/en/community/software" },
{ text: "Soldering", link: "/en/community/soldering" },
{ text: "Waterproofing", link: "/en/community/coating" },
{ text: "Law", link: "/en/community/law" },
{ text: "Traveling with drones", link: "/en/community/bag" },
],
},
/*{
text: "Shopping",
collapsed: false,
items: [
{ text: "Must-have items", link: "/shop/nessesary" },
{ text: "For Airplane Pilots", link: "/shop/airplanes" },
{ text: "Starter Kits", link: "/shop/set" },
{ text: "DIY Components", link: "/shop/DIY" },
],
},*/
{
text: "Settings",
collapsed: false,
collapsed: true,
items: [
// { text: "Beginner Questions", link: "/settings/FAQ" },
// { text: "Rates", link: "/settings/rates" },
// { text: "TX Scripts", link: "/settings/scripts" },
{ text: "For FrSky Users", link: "/settings/frsky" },
// { text: "Skyzone SteadyView", link: "/settings/skyzone" },
{ text: "Beginner Questions", link: "/en/settings/FAQ" },
{ text: "Rates", link: "/en/settings/rates" },
{ text: "TX Scripts", link: "/en/settings/scripts" },
{ text: "For FrSky Users", link: "/en/settings/frsky" },
{ text: "Skyzone SteadyView", link: "/en/settings/skyzone" },
],
},
];
Expand Down
10 changes: 5 additions & 5 deletions docs/.vitepress/config/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ function sidebar(): DefaultTheme.SidebarItem[] {
return [
{
text: "Введение",
collapsed: true,
collapsed: false,
items: [
{ text: "Вход в хобби", link: "/ru/intro/beginning" },
{ text: "Глоссарий", link: "/ru/intro/glossarium" },
{ text: "Стили полета", link: "/ru/intro/flightstyles" },
{ text: "Симулятор", link: "/ru/intro/sim" },
{ text: "Аппаратура", link: "/ru/intro/tx" },
Expand All @@ -50,7 +51,7 @@ function sidebar(): DefaultTheme.SidebarItem[] {
},
{
text: "Покупки",
collapsed: false,
collapsed: true,
items: [
{ text: "Must have вещи для хоббистов", link: "/ru/shop/nessesary" },
{ text: "Для самолетчиков", link: "/ru/shop/airplanes" },
Expand All @@ -60,9 +61,8 @@ function sidebar(): DefaultTheme.SidebarItem[] {
},
{
text: "Сообщество",
collapsed: false,
collapsed: true,
items: [
{ text: "Глоссарий", link: "/ru/community/glossarium" },
{ text: "Чаты", link: "/ru/community/chats" },
{ text: "Полезные материалы", link: "/ru/community/usefull" },
{ text: "Программное обеспечение", link: "/ru/community/software" },
Expand All @@ -74,7 +74,7 @@ function sidebar(): DefaultTheme.SidebarItem[] {
},
{
text: "Настройки",
collapsed: false,
collapsed: true,
items: [
{ text: "Вопросы новичков", link: "/ru/settings/FAQ" },
{ text: "Рейты", link: "/ru/settings/rates" },
Expand Down
159 changes: 147 additions & 12 deletions docs/.vitepress/config/shared.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,99 @@
import { defineConfig } from "vitepress";
import { search as ruSearch } from "./ru";
import { defineConfig, type HeadConfig } from "vitepress";
import { ViteImageOptimizer } from "vite-plugin-image-optimizer";
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
import { createContentLoader, type SiteConfig } from "vitepress";
import { Feed } from "feed";

export const shared = defineConfig({
rewrites: {
"en/:rest*": ":rest*",
},
rewrites: { "en/:rest*": ":rest*" },
ignoreDeadLinks: true,
lastUpdated: true,
cleanUrls: true,
metaChunk: true,

transformPageData(pageData) {
// 1. Meta Description
if (!pageData.frontmatter.description && pageData.filePath) {
try {
const fullPath = path.resolve(process.cwd(), "docs", pageData.filePath);
if (fs.existsSync(fullPath)) {
const description = excerpt(fs.readFileSync(fullPath, "utf-8"));
if (description) {
pageData.description = description;
pageData.frontmatter.description = description;
}
}
} catch (e: unknown) {
console.error(`Failed to generate description for ${pageData.filePath}`, e);
}
}

// 2. Simple Authors from Git
if (pageData.filePath) {
try {
const fullPath = path.resolve(process.cwd(), "docs", pageData.filePath);
const history = execSync(`git log --follow --format="%an|%ae" -- "${fullPath}"`, { encoding: "utf-8" })
.split("\n")
.map((l: string) => l.trim())
.filter(Boolean);

const emailsUsed = new Set<string>();
const namesUsed = new Set<string>();
const authors: string[] = [];

for (const line of history) {
const [name, email] = line.split("|");
if (!name || !email || email.includes("[bot]") || emailsUsed.has(email)) continue;

emailsUsed.add(email);
if (!namesUsed.has(name)) {
namesUsed.add(name);
authors.push(name);
}
}

if (authors.length > 0) {
pageData.frontmatter.gitAuthors = authors;
}
} catch {
// ignore errors during author extraction
}
}
},

transformHead({ pageData }) {
const head: HeadConfig[] = [];
if (pageData.description) {
head.push(["meta", { property: "og:description", content: pageData.description }]);
head.push(["meta", { name: "twitter:description", content: pageData.description }]);
}
if (pageData.title) {
head.push(["meta", { property: "og:title", content: pageData.title }]);
head.push(["meta", { name: "twitter:title", content: pageData.title }]);
}

const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: pageData.title,
description: pageData.description,
author: pageData.frontmatter.gitAuthors
? pageData.frontmatter.gitAuthors.map((name: string) => ({ "@type": "Person", name }))
: { "@type": "Organization", name: "PropWash Service" },
};
head.push(["script", { type: "application/ld+json" }, JSON.stringify(jsonLd)]);
return head;
},

buildEnd: async (config: SiteConfig) => {
await generateFeed(config);
},
sitemap: { hostname: "https://propwashservice.com" },
head: [
["link", { rel: "icon", type: "image/x-icon", href: "/favicon.svg" }],
["link", { rel: "alternate", type: "application/rss+xml", title: "RSS Feed", href: "/feed.rss" }],
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:locale", content: "en" }],
["meta", { property: "og:title", content: "DroneDocs" }],
Expand All @@ -24,7 +106,6 @@ export const shared = defineConfig({
{ type: "text/javascript" },
'(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(92209567, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true });',
],
// ["noscript", {}, ['div', {}, ["img", {src: "https://mc.yandex.ru/watch/92209567", style: "position:absolute; left:-9999px;"}]]],
],

themeConfig: {
Expand All @@ -41,16 +122,70 @@ export const shared = defineConfig({
{ icon: "youtube", link: "https://www.youtube.com/@PropWashService" },
],
search: {
provider: "algolia",
provider: "local",
options: {
appId: "T2WUNRWNAH",
apiKey: "682cbd880443f6771b3b4c37c137984e",
indexName: "propwashservice_ru",
locales: {
...ruSearch,
ru: {
translations: {
button: { buttonText: "Поиск", buttonAriaLabel: "Поиск" },
modal: {
noResultsText: "Нет результатов по запросу",
resetButtonTitle: "Сбросить поиск",
footer: { selectText: "выбрать", navigateText: "перейти", closeText: "закрыть" },
},
},
},
},
},
},
// carbonAds: { code: '', placement: '' }
},
vite: { plugins: [ViteImageOptimizer()] },
});

function excerpt(content: string) {
if (!content) return "";
const stripped = content
.replace(/^---[\s\S]*?---\n*/, "")
.replace(/<style[\s\S]*?<\/style>/gi, "")
.replace(/<script[\s\S]*?<\/script>/gi, "")
.replace(/#+ .*/g, "")
.replace(/!\[.*?\]\(.*?\)/g, "")
.replace(/\[(.*?)\]\(.*?\)/g, "$1")
.replace(/<.*?>/g, "")
.replace(/`{1,3}.*?`{1,3}/gs, "")
.replace(/&[a-z0-9#]+;/gi, "")
.replace(/\s+/g, " ")
.trim();
return stripped.length > 160 ? stripped.slice(0, 157) + "..." : stripped;
}

async function generateFeed(config: SiteConfig) {
const hostname = "https://propwashservice.com";
const feed = new Feed({
title: "DroneDocs",
description: "FPV Guides",
id: hostname,
link: hostname,
language: "ru",
image: `${hostname}/favicon.svg`,
favicon: `${hostname}/favicon.svg`,
copyright: "PropWash Service",
});
const posts = await createContentLoader("**/*.md", { excerpt: true, render: true }).load();
posts.sort((a, b) => +new Date(b.frontmatter.date || 0) - +new Date(a.frontmatter.date || 0));
for (const { url, excerpt, frontmatter, html } of posts) {
if (url.includes("/public/")) continue;
feed.addItem({
title: frontmatter.title || "DroneDocs Article",
id: `${hostname}${url}`,
link: `${hostname}${url}`,
description: excerpt,
content: html,
author: [{ name: "PropWash Service", link: hostname }],
date: frontmatter.date ? new Date(frontmatter.date) : new Date(),
});
}
const outDir = path.resolve(config.outDir);
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(path.join(outDir, "feed.rss"), feed.rss2());
}
Loading
Loading