vitepress-plugin-meta
Автоматически выставляет per-page head-теги для VitePress: description, Open Graph, Twitter Card, JSON-LD Article, canonical, robots, keywords, author, article:published_time / article:modified_time. По желанию рендерит per-page OG-картинку из SVG-шаблона через sharp.
Плагин закрывает пласт SEO/GEO, который в базовой теме VitePress приходится расписывать вручную в head каждой страницы. Метаданные резолвятся из frontmatter, если есть, иначе из первого абзаца исходника; уже проставленные теги не перетираются.
Установка
pnpm add @ampernic/vitepress-plugin-metaОпционально sharp -- нужен только если пользуетесь генерацией OG-картинок:
pnpm add -D sharpПакет sharp объявлен peer'ом с флагом optional. Если он не установлен, плагин пишет OG-картинку как SVG (браузеры и краулеры её поддерживают).
Скоуп @ampernic из Forgejo цепляется через .npmrc:
@ampernic:registry=https://altlinux.space/api/packages/ampernic/npm/Использование
Плагин возвращает transformPageData и buildEnd, которые кладутся в корень defineConfig:
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
import { MetaPlugin, productTemplate } from '@ampernic/vitepress-plugin-meta'
const meta = MetaPlugin({
hostname: 'https://docs.altlinux.space',
siteName: 'ALT Workstation',
locale: 'ru_RU',
twitter: { site: '@altlinux' },
ogImage: {
generate: {
template: productTemplate,
output: 'og/{slug}.png',
width: 1200,
height: 630,
},
},
})
export default defineConfig({
transformPageData: meta.transformPageData,
buildEnd: meta.buildEnd,
})При использовании @ampernic/vitepress-theme-alt-docs те же опции пробрасываются полем meta внутри createSharedConfig -- плагин уже примонтирован в тему.
Уже прописанные в frontmatter head-теги не перетираются: страница, объявившая свой og:image, оставляет его.
Опции
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
hostname | string | -- | Абсолютный origin для canonical и og:url. Без него оба тега не пишутся |
siteName | string | 'Documentation' | og:site_name и фолбэк описания |
locale | string | 'ru_RU' | og:locale |
fallbackDescription | string | Документация ${siteName} | Заглушка, если у страницы нет ни frontmatter-описания, ни первого абзаца |
emit | EmitToggles | все true | Флаги семейств: description, keywords, author, canonical, openGraph, twitter, jsonLd, robots, articleTime |
resolveTitle | (ctx) => string | -- | Хук над резолвом заголовка |
resolveDescription | (ctx) => string | -- | Хук над резолвом описания |
resolveKeywords | (ctx) => string[] | undefined | -- | Ключевые слова |
resolveAuthor | (ctx) => string | undefined | -- | Автор |
resolveImage | (ctx) => string | undefined | -- | Явный путь og:image |
resolveRobots | (ctx) => string | undefined | -- | Значение robots -- перекрывает robots.default |
resolveOgImageData | (ctx) => Partial<...> | -- | Данные для генератора картинки: tagline, theme, logo, logoSecondary |
extraHead | (ctx) => HeadConfig[] | -- | Кастомные теги, добавляемые в конец |
ogImage.default | string | -- | Сайт-уровневая fallback-картинка |
ogImage.defaultWidth / defaultHeight | number | -- | Размеры fallback-картинки |
ogImage.generate.template(data) | (data: OgImageData) => string | -- | Возвращает SVG-строку, которая рендерится в PNG через sharp. Без sharp пишется тот же SVG |
ogImage.generate.output | string | 'og/{slug}.png' | Шаблон пути OG-картинки в outDir |
ogImage.generate.width / height | number | 1200 / 630 | Размеры сгенерированной картинки |
ogImage.generate.filter | (ctx) => boolean | всегда true | Разрешает пропустить страницы (например, 404) |
ogImage.generate.fontDirs | string[] | [] | Каталоги со шрифтами для рендера. См. ниже |
robots.default | string | 'index,follow' | Дефолтная строка robots |
twitter.site / creator | string | -- | twitter:site / twitter:creator |
pwa | PwaOptions | -- | Иконки/manifest для PWA, автогенерация из одного исходного SVG |
Встроенные шаблоны
productTemplate и minimalTemplate импортируются из корня пакета. Оба читают data.theme (bg, bgAccent, fg, muted, accent, fontFamily), так что тема-потребитель ребрендит без форка шаблона:
import { productTemplate } from '@ampernic/vitepress-plugin-meta'
MetaPlugin({
ogImage: {
generate: {
template: productTemplate,
output: 'og/{slug}.png',
filter: (ctx) => !ctx.pageData.relativePath.startsWith('404'),
},
},
resolveOgImageData: () => ({
theme: {
bg: '#0d1117',
accent: '#fa814c',
fontFamily: 'Inter, sans-serif',
},
}),
})Порядок работы
transformPageDataчитает исходник страницы (учитывая vitepressrewrites), резолвит поля черезresolveAll, собираетheadчерезheadBuilder, дописывает его во frontmatter страницы.- Существующие head-записи с тем же
name/property/relсохраняются -- side-by-side frontmatter выигрывает у автогенерации. - Для каждой страницы, у которой сработал
filterOG-генератора, накапливается запись; приbuildEndона превращается в PNG (черезsharp) или SVG (без него). - Если задан
pwa.generate, тот жеbuildEndгенерирует иконки иsite.webmanifestвoutDir.
Нюансы
hostnameпропускать нельзя, если сайт хочет валидныйog:urlиcanonical.articleTimeберётся из git-метаданных исходника; отключите флагомemit.articleTime, если репозиторий без истории коммитов.- OG-картинка не перерисовывается, если
resolveImageвернул готовый URL: он ставится напрямую какog:image. - Для канонических URL с локалью учитывайте
vitepress.rewrites:ru/index.mdрендерится как/, и плагин уже применяет прямой rewrite.
Шрифты в картинке
sharp резолвит гарнитуры через системный fontconfig и игнорирует@font-face внутри SVG. Поэтому шрифт, которого нет в системе сборки, молча подменяется на первый подходящий - и картинка уезжает от фирменного стиля.
fontDirs указывает каталоги, которые нужно добавить к поиску:
ogImage: {
generate: {
template: myTemplate,
fontDirs: ['/path/to/fonts'],
},
}Плагин собирает временный конфиг, включающий системный, так что обычные запасные гарнитуры продолжают работать. Каталоги подключаются один раз за процесс, до первой отрисовки текста.
Кладите статические начертания в TrueType. Вариативный шрифт fontconfig индексирует под именем инстанса по умолчанию (Montserrat Thin), и запрос базового семейства его не находит.