Add SEO: OG tags, Twitter Card, hreflang, JSON-LD, sitemap
- config.ts: og:site_name, og:image (+w/h/alt), twitter:card/site/image - config.ts: transformPageData — per-page canonical, og:url, og:locale, og:locale:alternate, hreflang for all 6 locales + x-default, og:title/description and twitter:title/description from frontmatter - config.ts: sitemap with xhtml:link alternates for all 174 pages × 6 locales - SchemaOrg.vue: SoftwareApplication JSON-LD on EN homepage only - HomeLayout.vue: render SchemaOrg in home-features-after slot - index.md + de/fr/es/it/pl index.md: add translated title to frontmatter
This commit is contained in:
parent
940d0de5b6
commit
513e0b6f4a
166
openspec/changes/archive/2026-03-07-seo-meta-og/design.md
Normal file
166
openspec/changes/archive/2026-03-07-seo-meta-og/design.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# Design: seo-meta-og
|
||||
|
||||
## Scope
|
||||
|
||||
Changes only in `packages/docs/`. No app code touched.
|
||||
|
||||
## URL structure
|
||||
|
||||
```
|
||||
https://openpencil.dev/ → EN (root, x-default)
|
||||
https://openpencil.dev/de/ → DE
|
||||
https://openpencil.dev/fr/ → FR
|
||||
https://openpencil.dev/es/ → ES
|
||||
https://openpencil.dev/it/ → IT
|
||||
https://openpencil.dev/pl/ → PL
|
||||
```
|
||||
|
||||
Page paths follow the same pattern: `/guide/features` (EN), `/de/guide/features` (DE), etc.
|
||||
|
||||
## Locale map
|
||||
|
||||
```ts
|
||||
const LOCALES = {
|
||||
en: { lang: 'en', hreflang: 'en', ogLocale: 'en_US', prefix: '' },
|
||||
de: { lang: 'de', hreflang: 'de', ogLocale: 'de_DE', prefix: '/de' },
|
||||
fr: { lang: 'fr', hreflang: 'fr', ogLocale: 'fr_FR', prefix: '/fr' },
|
||||
es: { lang: 'es', hreflang: 'es', ogLocale: 'es_ES', prefix: '/es' },
|
||||
it: { lang: 'it', hreflang: 'it', ogLocale: 'it_IT', prefix: '/it' },
|
||||
pl: { lang: 'pl', hreflang: 'pl', ogLocale: 'pl_PL', prefix: '/pl' },
|
||||
}
|
||||
const BASE = 'https://openpencil.dev'
|
||||
const LOCALE_PREFIXES = ['de', 'fr', 'es', 'it', 'pl']
|
||||
```
|
||||
|
||||
## 1. Global head tags (`config.ts`)
|
||||
|
||||
Add to the root `head: []`:
|
||||
|
||||
```ts
|
||||
['meta', { property: 'og:site_name', content: 'OpenPencil' }],
|
||||
['meta', { property: 'og:image', content: 'https://openpencil.dev/screenshot.png' }],
|
||||
['meta', { property: 'og:image:width', content: '2784' }],
|
||||
['meta', { property: 'og:image:height', content: '1824' }],
|
||||
['meta', { property: 'og:image:alt', content: 'OpenPencil — AI-Native Design Editor' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@openpencildev' }],
|
||||
['meta', { name: 'twitter:image', content: 'https://openpencil.dev/screenshot.png' }],
|
||||
```
|
||||
|
||||
Keep existing: favicon, `og:type`, `og:title`, `og:description`.
|
||||
|
||||
## 2. `transformPageData` hook
|
||||
|
||||
Fires at build time for every page. Logic:
|
||||
|
||||
```ts
|
||||
transformPageData(pageData) {
|
||||
const rel = pageData.relativePath // e.g. 'de/guide/features.md'
|
||||
|
||||
// Determine current locale
|
||||
const localeKey = LOCALE_PREFIXES.find(p => rel.startsWith(p + '/')) ?? 'en'
|
||||
const locale = LOCALES[localeKey]
|
||||
|
||||
// Slug = path without locale prefix and .md, index → ''
|
||||
const slug = rel
|
||||
.replace(/^(de|fr|es|it|pl)\//, '')
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/\/index$/, '')
|
||||
.replace(/^index$/, '')
|
||||
|
||||
const pageUrl = `${BASE}${locale.prefix}/${slug}`.replace(/\/$/, '') || BASE
|
||||
const enUrl = slug ? `${BASE}/${slug}` : BASE
|
||||
|
||||
pageData.frontmatter.head ??= []
|
||||
const h = pageData.frontmatter.head
|
||||
|
||||
// Canonical (locale-specific)
|
||||
h.push(['link', { rel: 'canonical', href: pageUrl }])
|
||||
|
||||
// og:url (locale-specific)
|
||||
h.push(['meta', { property: 'og:url', content: pageUrl }])
|
||||
|
||||
// og:locale (current locale)
|
||||
h.push(['meta', { property: 'og:locale', content: locale.ogLocale }])
|
||||
|
||||
// og:locale:alternate for other locales
|
||||
for (const [key, loc] of Object.entries(LOCALES)) {
|
||||
if (key !== localeKey) {
|
||||
h.push(['meta', { property: 'og:locale:alternate', content: loc.ogLocale }])
|
||||
}
|
||||
}
|
||||
|
||||
// hreflang for all locales + x-default
|
||||
for (const [key, loc] of Object.entries(LOCALES)) {
|
||||
const altUrl = slug ? `${BASE}${loc.prefix}/${slug}` : `${BASE}${loc.prefix || ''}`
|
||||
h.push(['link', { rel: 'alternate', hreflang: loc.hreflang, href: altUrl.replace(/\/$/, '') || BASE }])
|
||||
}
|
||||
// x-default points to EN
|
||||
h.push(['link', { rel: 'alternate', hreflang: 'x-default', href: enUrl || BASE }])
|
||||
|
||||
// og:title (per page)
|
||||
if (pageData.title) {
|
||||
h.push(['meta', { property: 'og:title', content: `${pageData.title} — OpenPencil` }])
|
||||
h.push(['meta', { name: 'twitter:title', content: `${pageData.title} — OpenPencil` }])
|
||||
}
|
||||
|
||||
// og:description + meta description (per page)
|
||||
if (pageData.description) {
|
||||
h.push(['meta', { property: 'og:description', content: pageData.description }])
|
||||
h.push(['meta', { name: 'twitter:description', content: pageData.description }])
|
||||
h.push(['meta', { name: 'description', content: pageData.description }])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Sitemap with hreflang alternates
|
||||
|
||||
```ts
|
||||
sitemap: {
|
||||
hostname: BASE,
|
||||
transformItems(items) {
|
||||
return items.map(item => {
|
||||
// Determine slug and locale from url
|
||||
const localeKey = LOCALE_PREFIXES.find(p => item.url.startsWith('/' + p + '/')) ?? 'en'
|
||||
const slug = item.url.replace(/^\/(de|fr|es|it|pl)\//, '/').replace(/\/$/, '') || '/'
|
||||
|
||||
return {
|
||||
...item,
|
||||
links: Object.entries(LOCALES).map(([, loc]) => ({
|
||||
lang: loc.hreflang,
|
||||
url: `${BASE}${loc.prefix}${slug === '/' ? '' : slug}` || BASE,
|
||||
})),
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 4. JSON-LD component (`SchemaOrg.vue`)
|
||||
|
||||
Renders a `<script type="application/ld+json">` only on the EN homepage. Uses VitePress `useData()` to detect the page. The `<component :is="'script'">` pattern avoids Vue treating it as a special element.
|
||||
|
||||
Placed via `home-features-after` slot in `HomeLayout.vue` (already used for the screenshot). Only renders when `frontmatter.layout === 'home'` AND locale is EN (no prefix in `page.relativePath`).
|
||||
|
||||
## 5. Homepage frontmatter (`index.md` and locale index files)
|
||||
|
||||
Root `index.md` — add `title` for cleaner `<title>` tag:
|
||||
```yaml
|
||||
title: OpenPencil — AI-Native Design Editor
|
||||
```
|
||||
|
||||
Locale index files (`de/index.md`, `fr/index.md`, etc.) — add translated `title`:
|
||||
- DE: `OpenPencil — KI-nativer Design-Editor`
|
||||
- FR: `OpenPencil — Éditeur de Design IA-Natif`
|
||||
- ES: `OpenPencil — Editor de Diseño IA-Nativo`
|
||||
- IT: `OpenPencil — Editor di Design IA-Nativo`
|
||||
- PL: `OpenPencil — Edytor Graficzny z Natywnym AI`
|
||||
|
||||
## Key decisions
|
||||
|
||||
- **`hreflang` on every page**: Required for Google to correctly associate locale variants. VitePress `transformPageData` is the right hook — fires at build time, modifies `frontmatter.head` which VitePress renders into `<head>`.
|
||||
- **`x-default` → EN**: Standard practice; tells Google the canonical "fallback" language.
|
||||
- **`og:locale:alternate`**: Facebook/OG crawlers use this to link locale variants.
|
||||
- **Sitemap alternates**: `xhtml:link` entries in sitemap are redundant with hreflang in HTML but recommended by Google for completeness.
|
||||
- **JSON-LD only on EN homepage**: Avoid duplicate structured data across locales; Google recommends one authoritative schema page.
|
||||
- **`twitter:title` / `twitter:description`**: Per-page Twitter card content, separate from global og tags.
|
||||
25
openspec/changes/archive/2026-03-07-seo-meta-og/proposal.md
Normal file
25
openspec/changes/archive/2026-03-07-seo-meta-og/proposal.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Proposal: seo-meta-og
|
||||
|
||||
## Problem
|
||||
|
||||
The VitePress docs site at `openpencil.dev` has minimal SEO and social metadata:
|
||||
|
||||
- **Open Graph tags** are incomplete — `og:image`, `og:url`, `og:site_name`, `twitter:card`, `twitter:image` are all missing. Social shares on X/Twitter, Slack, Discord show no preview image.
|
||||
- **Structured data (JSON-LD)** is absent — no `SoftwareApplication` schema, so Google can't render rich results for the app.
|
||||
- **Canonical URL** is missing — no `og:url` or `<link rel="canonical">` per page.
|
||||
- **Per-page meta descriptions** — many pages have frontmatter `title` + `description`, but non-frontmatter pages (guides, reference) have no `<meta name="description">`. VitePress does pick up frontmatter but the site-level fallback is fine; the real gap is OG tags.
|
||||
- **Twitter/X Card** — `twitter:card`, `twitter:site`, `twitter:image` not set. Links pasted to X show no card.
|
||||
- **Sitemap** — VitePress supports `sitemap` config but it isn't enabled. Google can only discover pages by crawling.
|
||||
- **`og:image`** — `screenshot.png` (2784×1824) already exists in `public/` and is perfect for OG image (recommended: 1200×630 crop or use as-is since most crawlers handle oversized).
|
||||
|
||||
## What changes
|
||||
|
||||
1. **`config.ts`** — add complete `head[]` array with OG, Twitter Card, and canonical base URL; enable sitemap; add `og:image` pointing to `/screenshot.png`
|
||||
2. **`config.ts`** — add `transformPageData` hook to inject per-page `og:title`, `og:description`, `og:url` dynamically
|
||||
3. **`index.md`** — add frontmatter `title` for cleaner OG title on homepage
|
||||
4. **JSON-LD component** — `SchemaOrg.vue` injecting `SoftwareApplication` structured data on the homepage
|
||||
5. **`theme/index.ts`** — register SchemaOrg component for homepage
|
||||
|
||||
## Why
|
||||
|
||||
Search engines and social platforms rely on these signals. Without OG image, links to `openpencil.dev` shared anywhere show a blank card. Without JSON-LD, Google won't show the app in rich results. Sitemap speeds up discovery of all 50+ doc pages.
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# vitepress-docs — delta spec (seo-meta-og)
|
||||
|
||||
## Requirement: Open Graph image metadata
|
||||
|
||||
The docs site SHALL include `og:image`, `og:image:width`, `og:image:height`, and `og:image:alt` globally, pointing to `https://openpencil.dev/screenshot.png`.
|
||||
|
||||
### Scenario: OG image tag present on any page
|
||||
- **WHEN** a crawler fetches any page on `openpencil.dev`
|
||||
- **THEN** `<meta property="og:image" content="https://openpencil.dev/screenshot.png">` is in `<head>`
|
||||
|
||||
## Requirement: Twitter/X Card metadata
|
||||
|
||||
The docs site SHALL include `twitter:card` (value: `summary_large_image`), `twitter:site`, and `twitter:image` globally. Per-page `twitter:title` and `twitter:description` SHALL be injected from the page title and description.
|
||||
|
||||
### Scenario: Twitter card renders with large image
|
||||
- **WHEN** a `openpencil.dev` URL is shared on X/Twitter
|
||||
- **THEN** a large-image card is shown with the screenshot preview
|
||||
|
||||
## Requirement: og:site_name
|
||||
|
||||
The docs site SHALL include `<meta property="og:site_name" content="OpenPencil">` globally.
|
||||
|
||||
## Requirement: hreflang alternate links on every page
|
||||
|
||||
Every page SHALL include `<link rel="alternate" hreflang="...">` tags for all 6 language variants (en, de, fr, es, it, pl) plus `hreflang="x-default"` pointing to the EN version. The `x-default` href SHALL point to the English URL.
|
||||
|
||||
### Scenario: EN content page has all hreflang links
|
||||
- **WHEN** crawler fetches `/guide/features`
|
||||
- **THEN** hreflang links for en/de/fr/es/it/pl and x-default are all present in `<head>`
|
||||
|
||||
### Scenario: DE content page has correct hreflang
|
||||
- **WHEN** crawler fetches `/de/guide/features`
|
||||
- **THEN** hreflang links point to their respective locale URLs (e.g. de → `/de/guide/features`, en → `/guide/features`)
|
||||
|
||||
### Scenario: x-default always points to EN
|
||||
- **WHEN** crawler fetches any locale page
|
||||
- **THEN** `hreflang="x-default"` href is the English URL (no locale prefix)
|
||||
|
||||
## Requirement: og:locale and og:locale:alternate
|
||||
|
||||
Every page SHALL include `og:locale` matching the page language (e.g. `de_DE` for DE pages). All other locales SHALL be listed as `og:locale:alternate`.
|
||||
|
||||
### Scenario: DE page has correct og:locale
|
||||
- **WHEN** crawler fetches `/de/guide/features`
|
||||
- **THEN** `og:locale` is `de_DE` and other locales appear as `og:locale:alternate`
|
||||
|
||||
## Requirement: Per-page canonical URL
|
||||
|
||||
Every page SHALL have `<link rel="canonical">` with the full `https://openpencil.dev/<locale-prefix>/<slug>` URL. The canonical SHALL be locale-specific (DE pages have DE canonical).
|
||||
|
||||
### Scenario: DE page canonical is locale-specific
|
||||
- **WHEN** crawler fetches `/de/guide/features`
|
||||
- **THEN** canonical is `https://openpencil.dev/de/guide/features` (not the EN URL)
|
||||
|
||||
## Requirement: Per-page og:url
|
||||
|
||||
Every page SHALL have `<meta property="og:url">` matching the page's canonical URL.
|
||||
|
||||
## Requirement: Per-page og:title and og:description
|
||||
|
||||
Pages with a title SHALL have `og:title` set to `{title} — OpenPencil`. Pages with `description` frontmatter SHALL have `og:description`, `twitter:description`, and `meta name="description"` from it.
|
||||
|
||||
### Scenario: Content page OG title
|
||||
- **WHEN** crawler fetches `/user-guide/canvas-navigation`
|
||||
- **THEN** `og:title` is `Canvas Navigation — OpenPencil`
|
||||
|
||||
## Requirement: Sitemap with locale alternates
|
||||
|
||||
The docs site SHALL generate `sitemap.xml` with `<xhtml:link rel="alternate">` entries for all locale variants of each page.
|
||||
|
||||
### Scenario: Sitemap generated with alternates
|
||||
- **WHEN** `bun run docs:build` completes
|
||||
- **THEN** `sitemap.xml` exists and each URL entry contains `<xhtml:link>` alternates for all 6 locales
|
||||
|
||||
## Requirement: JSON-LD SoftwareApplication schema on EN homepage
|
||||
|
||||
The EN homepage SHALL include a `<script type="application/ld+json">` with `SoftwareApplication` schema: name, applicationCategory, operatingSystem, offers (free), url, description, screenshot, license. It SHALL NOT appear on locale homepages to avoid duplicate schema.
|
||||
|
||||
### Scenario: Structured data on EN homepage
|
||||
- **WHEN** crawler fetches `https://openpencil.dev/`
|
||||
- **THEN** a valid `SoftwareApplication` JSON-LD block is present
|
||||
|
||||
### Scenario: No duplicate schema on locale homepage
|
||||
- **WHEN** crawler fetches `https://openpencil.dev/de/`
|
||||
- **THEN** NO `SoftwareApplication` JSON-LD block is present
|
||||
|
||||
## Requirement: Translated title on locale homepages
|
||||
|
||||
Each locale homepage `index.md` SHALL have a translated `title` frontmatter field so the `<title>` tag is in the correct language.
|
||||
|
||||
### Scenario: DE homepage title in German
|
||||
- **WHEN** browser opens `https://openpencil.dev/de/`
|
||||
- **THEN** `<title>OpenPencil — KI-nativer Design-Editor | OpenPencil</title>` (or equivalent) is in `<head>`
|
||||
39
openspec/changes/archive/2026-03-07-seo-meta-og/tasks.md
Normal file
39
openspec/changes/archive/2026-03-07-seo-meta-og/tasks.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Tasks: seo-meta-og
|
||||
|
||||
## 1. Global head tags + transformPageData
|
||||
|
||||
- [x] 1.1 `packages/docs/.vitepress/config.ts` — add to root `head[]`: `og:site_name`, `og:image` (+ width/height/alt), `twitter:card`, `twitter:site`, `twitter:image`
|
||||
- [x] 1.2 `packages/docs/.vitepress/config.ts` — add `transformPageData` hook that for every page injects into `frontmatter.head`:
|
||||
- `<link rel="canonical">` with full locale-specific URL
|
||||
- `<meta property="og:url">` with full locale-specific URL
|
||||
- `<meta property="og:locale">` matching current page language (e.g. `de_DE`)
|
||||
- `<meta property="og:locale:alternate">` for all other 5 locales
|
||||
- `<link rel="alternate" hreflang="...">` for all 6 locales (en/de/fr/es/it/pl) pointing to their respective URLs
|
||||
- `<link rel="alternate" hreflang="x-default">` pointing to EN URL
|
||||
- `<meta property="og:title">` as `{pageData.title} — OpenPencil` (when title exists)
|
||||
- `<meta name="twitter:title">` same value
|
||||
- `<meta property="og:description">` from `pageData.description` (when exists)
|
||||
- `<meta name="twitter:description">` same value
|
||||
- `<meta name="description">` same value
|
||||
|
||||
## 2. Sitemap
|
||||
|
||||
- [x] 2.1 `packages/docs/.vitepress/config.ts` — add `sitemap: { hostname: 'https://openpencil.dev', transformItems }` where `transformItems` adds `links` array with all 6 locale alternates per page URL
|
||||
|
||||
## 3. JSON-LD structured data
|
||||
|
||||
- [x] 3.1 Create `packages/docs/.vitepress/theme/SchemaOrg.vue` — `SoftwareApplication` JSON-LD rendered only when `page.relativePath === 'index.md'` (EN homepage only, not locale homepages)
|
||||
- [x] 3.2 `packages/docs/.vitepress/theme/HomeLayout.vue` — import `SchemaOrg` and add `<SchemaOrg />` inside the `home-features-after` template slot (alongside existing screenshot)
|
||||
|
||||
## 4. Homepage titles
|
||||
|
||||
- [x] 4.1 `packages/docs/index.md` — add `title: OpenPencil — AI-Native Design Editor` to frontmatter
|
||||
- [x] 4.2 `packages/docs/de/index.md` — add `title: OpenPencil — KI-nativer Design-Editor`
|
||||
- [x] 4.3 `packages/docs/fr/index.md` — add `title: OpenPencil — Éditeur de Design IA-Natif`
|
||||
- [x] 4.4 `packages/docs/es/index.md` — add `title: OpenPencil — Editor de Diseño IA-Nativo`
|
||||
- [x] 4.5 `packages/docs/it/index.md` — add `title: OpenPencil — Editor di Design IA-Nativo`
|
||||
- [x] 4.6 `packages/docs/pl/index.md` — add `title: OpenPencil — Edytor Graficzny z Natywnym AI`
|
||||
|
||||
## 5. Verify build
|
||||
|
||||
- [x] 5.1 Run `cd packages/docs && bun run build` — confirm: exits 0, `sitemap.xml` exists in dist, `dist/index.html` contains `og:image`, `hreflang`, JSON-LD block, `dist/de/index.html` contains `og:locale` = `de_DE`, hreflang links, NO JSON-LD block
|
||||
|
|
@ -139,6 +139,19 @@ const ES: SidebarLabels = { gettingAround: 'Orientación', creatingContent: 'Cre
|
|||
|
||||
const PL: SidebarLabels = { gettingAround: 'Nawigacja', creatingContent: 'Tworzenie treści', organizing: 'Organizacja', advanced: 'Zaawansowane', canvasNav: 'Nawigacja po płótnie', selection: 'Zaznaczanie i edycja', shapes: 'Rysowanie kształtów', text: 'Edycja tekstu', pen: 'Narzędzie pióro', layers: 'Warstwy i strony', contextMenu: 'Menu kontekstowe', exporting: 'Eksportowanie', autoLayout: 'Auto-layout', components: 'Komponenty', variables: 'Zmienne', guide: 'Przewodnik', gettingStarted: 'Rozpoczęcie pracy', features: 'Funkcje', architecture: 'Architektura', techStack: 'Stack technologiczny', comparison: 'Porównanie', figmaMatrix: 'Matryca funkcji Figma' }
|
||||
|
||||
const BASE = 'https://openpencil.dev'
|
||||
|
||||
const LOCALE_PREFIXES = ['de', 'fr', 'es', 'it', 'pl'] as const
|
||||
|
||||
const LOCALES: Record<string, { hreflang: string; ogLocale: string; prefix: string }> = {
|
||||
en: { hreflang: 'en', ogLocale: 'en_US', prefix: '' },
|
||||
de: { hreflang: 'de', ogLocale: 'de_DE', prefix: '/de' },
|
||||
fr: { hreflang: 'fr', ogLocale: 'fr_FR', prefix: '/fr' },
|
||||
es: { hreflang: 'es', ogLocale: 'es_ES', prefix: '/es' },
|
||||
it: { hreflang: 'it', ogLocale: 'it_IT', prefix: '/it' },
|
||||
pl: { hreflang: 'pl', ogLocale: 'pl_PL', prefix: '/pl' },
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
title: 'OpenPencil',
|
||||
description: 'Open-source, AI-native design editor. Figma alternative built from scratch with full .fig file compatibility.',
|
||||
|
|
@ -146,13 +159,86 @@ export default defineConfig({
|
|||
lastUpdated: true,
|
||||
appearance: 'dark',
|
||||
|
||||
sitemap: {
|
||||
hostname: BASE,
|
||||
transformItems(items) {
|
||||
return items.map((item) => {
|
||||
const localeKey = LOCALE_PREFIXES.find((p) => item.url.startsWith(p + '/')) ?? 'en'
|
||||
const slug = item.url
|
||||
.replace(new RegExp(`^(${LOCALE_PREFIXES.join('|')})/`), '')
|
||||
.replace(/\/$/, '')
|
||||
|
||||
return {
|
||||
...item,
|
||||
links: Object.entries(LOCALES).map(([, loc]) => {
|
||||
const url = slug ? `${BASE}${loc.prefix}/${slug}` : `${BASE}${loc.prefix || '/'}`
|
||||
return { lang: loc.hreflang, url }
|
||||
}),
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
head: [
|
||||
['link', { rel: 'icon', type: 'image/png', href: '/favicon.png' }],
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:title', content: 'OpenPencil' }],
|
||||
['meta', { property: 'og:description', content: 'Open-source, AI-native design editor' }],
|
||||
['meta', { property: 'og:site_name', content: 'OpenPencil' }],
|
||||
['meta', { property: 'og:image', content: `${BASE}/screenshot.png` }],
|
||||
['meta', { property: 'og:image:width', content: '2784' }],
|
||||
['meta', { property: 'og:image:height', content: '1824' }],
|
||||
['meta', { property: 'og:image:alt', content: 'OpenPencil — AI-Native Design Editor' }],
|
||||
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
|
||||
['meta', { name: 'twitter:site', content: '@openpencildev' }],
|
||||
['meta', { name: 'twitter:image', content: `${BASE}/screenshot.png` }],
|
||||
],
|
||||
|
||||
transformPageData(pageData) {
|
||||
const rel = pageData.relativePath
|
||||
|
||||
const localeKey = (LOCALE_PREFIXES.find((p) => rel.startsWith(p + '/')) as string) ?? 'en'
|
||||
const locale = LOCALES[localeKey]
|
||||
|
||||
const slug = rel
|
||||
.replace(new RegExp(`^(${LOCALE_PREFIXES.join('|')})/`), '')
|
||||
.replace(/\.md$/, '')
|
||||
.replace(/\/index$/, '')
|
||||
.replace(/^index$/, '')
|
||||
|
||||
const pageUrl = slug ? `${BASE}${locale.prefix}/${slug}` : `${BASE}${locale.prefix || ''}`
|
||||
const enSlug = slug ? `${BASE}/${slug}` : BASE
|
||||
|
||||
pageData.frontmatter.head ??= []
|
||||
const h = pageData.frontmatter.head as [string, Record<string, string>][]
|
||||
|
||||
h.push(['link', { rel: 'canonical', href: pageUrl }])
|
||||
h.push(['meta', { property: 'og:url', content: pageUrl }])
|
||||
h.push(['meta', { property: 'og:locale', content: locale.ogLocale }])
|
||||
|
||||
for (const [key, loc] of Object.entries(LOCALES)) {
|
||||
if (key !== localeKey) {
|
||||
h.push(['meta', { property: 'og:locale:alternate', content: loc.ogLocale }])
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, loc] of Object.entries(LOCALES)) {
|
||||
const altUrl = slug ? `${BASE}${loc.prefix}/${slug}` : `${BASE}${loc.prefix || ''}`
|
||||
h.push(['link', { rel: 'alternate', hreflang: loc.hreflang, href: altUrl }])
|
||||
}
|
||||
h.push(['link', { rel: 'alternate', hreflang: 'x-default', href: enSlug }])
|
||||
|
||||
if (pageData.title) {
|
||||
const ogTitle = `${pageData.title} — OpenPencil`
|
||||
h.push(['meta', { property: 'og:title', content: ogTitle }])
|
||||
h.push(['meta', { name: 'twitter:title', content: ogTitle }])
|
||||
}
|
||||
|
||||
if (pageData.description) {
|
||||
h.push(['meta', { property: 'og:description', content: pageData.description }])
|
||||
h.push(['meta', { name: 'twitter:description', content: pageData.description }])
|
||||
h.push(['meta', { name: 'description', content: pageData.description }])
|
||||
}
|
||||
},
|
||||
|
||||
locales: {
|
||||
root: {
|
||||
label: 'English',
|
||||
|
|
@ -174,7 +260,7 @@ export default defineConfig({
|
|||
label: 'Français',
|
||||
lang: 'fr',
|
||||
description: 'Éditeur de design open-source, IA-natif. Alternative à Figma.',
|
||||
themeConfig: localeThemeConfig('/fr', { userGuide: 'Guide utilisateur', reference: 'Référence', development: 'Développement', openApp: 'Ouvrir l\'app' }, FR),
|
||||
themeConfig: localeThemeConfig('/fr', { userGuide: 'Guide utilisateur', reference: 'Référence', development: 'Développement', openApp: "Ouvrir l'app" }, FR),
|
||||
},
|
||||
es: {
|
||||
label: 'Español',
|
||||
|
|
@ -185,7 +271,7 @@ export default defineConfig({
|
|||
pl: {
|
||||
label: 'Polski',
|
||||
lang: 'pl',
|
||||
description: 'Open-source\'owy edytor graficzny z natywnym AI. Alternatywa dla Figmy.',
|
||||
description: "Open-source'owy edytor graficzny z natywnym AI. Alternatywa dla Figmy.",
|
||||
themeConfig: localeThemeConfig('/pl', { userGuide: 'Podręcznik', reference: 'Referencja', development: 'Rozwój', openApp: 'Otwórz app' }, PL),
|
||||
},
|
||||
},
|
||||
|
|
@ -229,9 +315,7 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/open-pencil/open-pencil' },
|
||||
],
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/open-pencil/open-pencil' }],
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/open-pencil/open-pencil/edit/main/packages/docs/:path',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import DefaultTheme from 'vitepress/theme'
|
||||
import { useData } from 'vitepress'
|
||||
import SchemaOrg from './SchemaOrg.vue'
|
||||
|
||||
const { Layout } = DefaultTheme
|
||||
const { frontmatter } = useData()
|
||||
|
|
@ -16,6 +17,9 @@ const { frontmatter } = useData()
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #home-features-after>
|
||||
<SchemaOrg />
|
||||
</template>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
39
packages/docs/.vitepress/theme/SchemaOrg.vue
Normal file
39
packages/docs/.vitepress/theme/SchemaOrg.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<script setup lang="ts">
|
||||
import { useData } from 'vitepress'
|
||||
|
||||
const { page } = useData()
|
||||
|
||||
const isEnHome = page.value.relativePath === 'index.md'
|
||||
|
||||
const schema = JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'OpenPencil',
|
||||
applicationCategory: 'DesignApplication',
|
||||
operatingSystem: 'Windows, macOS, Linux, Web',
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
price: '0',
|
||||
priceCurrency: 'USD',
|
||||
},
|
||||
url: 'https://app.openpencil.dev',
|
||||
description:
|
||||
'Open-source, AI-native design editor. Figma-compatible with full .fig file support.',
|
||||
softwareVersion: '0.7.0',
|
||||
license: 'https://opensource.org/licenses/MIT',
|
||||
screenshot: 'https://openpencil.dev/screenshot.png',
|
||||
downloadUrl: 'https://github.com/open-pencil/open-pencil/releases/latest',
|
||||
featureList: [
|
||||
'Open .fig files natively',
|
||||
'AI chat with 78 design tools',
|
||||
'MCP server for AI coding tools',
|
||||
'P2P real-time collaboration',
|
||||
'Headless CLI for automation',
|
||||
'Desktop app via Tauri',
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="'script'" v-if="isEnHome" type="application/ld+json">{{ schema }}</component>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — KI-nativer Design-Editor
|
||||
description: Open-Source Figma-Alternative. Vollständig lokal, KI-nativ, programmierbar.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — Editor de Diseño IA-Nativo
|
||||
description: Alternativa open-source a Figma. Completamente local, IA-nativa, programable.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — Éditeur de Design IA-Natif
|
||||
description: Alternative open-source à Figma. Entièrement local, IA-native, programmable.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — AI-Native Design Editor
|
||||
description: Open-source Figma alternative. Fully local, AI-native, programmable.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — Editor di Design IA-Nativo
|
||||
description: Alternativa open-source a Figma. Completamente locale, IA-nativa, programmabile.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
layout: home
|
||||
title: OpenPencil — Edytor Graficzny z Natywnym AI
|
||||
description: Open-source'owa alternatywa dla Figma. W pełni lokalna, natywnie AI, programowalna.
|
||||
|
||||
hero:
|
||||
|
|
|
|||
Loading…
Reference in a new issue