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:
Anton A S 2026-03-07 19:49:24 +03:00
parent 940d0de5b6
commit 513e0b6f4a
13 changed files with 463 additions and 7 deletions

View 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.

View 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.

View file

@ -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>`

View 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

View file

@ -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',

View file

@ -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>

View 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>

View file

@ -1,5 +1,6 @@
---
layout: home
title: OpenPencil — KI-nativer Design-Editor
description: Open-Source Figma-Alternative. Vollständig lokal, KI-nativ, programmierbar.
hero:

View file

@ -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:

View file

@ -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:

View file

@ -1,5 +1,6 @@
---
layout: home
title: OpenPencil — AI-Native Design Editor
description: Open-source Figma alternative. Fully local, AI-native, programmable.
hero:

View file

@ -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:

View file

@ -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: