feat(app): prepare Vercel deployment

Add production metadata, social assets, Vercel observability, repository links, and final navigation and drawer polish.
This commit is contained in:
smontlouis 2026-08-13 20:08:26 +02:00
parent 94bc5223e6
commit 3c5f5cb41e
14 changed files with 187 additions and 10 deletions

View File

@ -24,7 +24,7 @@ jobs:
node-version: '22.12.0'
- name: Install pnpm
run: npm install --global pnpm@11.21.0
run: npm install --global pnpm@10.34.5
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@ -4,6 +4,7 @@ legacy/
LICENSE
public/favicon.svg
public/robots.txt
public/sitemap.xml
src/features/studio/defaultStudioDocument.json
src/features/export/standaloneEngine.generated.ts
pnpm-lock.yaml

View File

@ -2,6 +2,8 @@
Bible Strong Avatar Lab is a browser-based authoring studio for procedural 2D avatars. It combines 3D-inspired geometry with SVG rendering so you can construct a character, define its neutral appearance, create expressions, compose reusable animations, and export the result without depending on the Studio UI.
Website: [bible-strong.app](https://bible-strong.app) · Source: [GitHub](https://github.com/smontlouis/bible-strong-avatar-lab)
The application runs entirely in the browser. Projects are stored locally and can be moved between browsers with JSON export/import; no account or backend is required.
## What you can do
@ -64,7 +66,7 @@ The JSON project file contains the complete current document: avatars, base and
### Requirements
- Node.js 22.12 or newer
- pnpm 11.21 (declared by the repository's `packageManager` field)
- pnpm 10.34 (declared by the repository's `packageManager` field)
### Install and run
@ -116,6 +118,8 @@ pnpm preview
The application is a client-only Vite site with no server runtime or environment variables. The build uses relative asset paths, so the contents of `dist/` can be hosted at a domain root or a subpath, including a GitHub Pages project URL. Any static host that serves `index.html` can deploy it.
Vercel Web Analytics and Speed Insights are integrated in the React entry point. Enable both products in the Vercel project dashboard, then redeploy so their collection routes become available.
## Technical overview
- **React 19** coordinates the editor UI and durable application state.

View File

@ -10,20 +10,36 @@
content="Create procedural 2D avatars, expressions and animations in your browser, then export reusable React, JavaScript, SVG and PNG assets."
/>
<meta name="application-name" content="Bible Strong Avatar Lab" />
<meta name="author" content="Stéphane Montlouis-Calixte" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:url" content="https://bible-strong.app/" />
<meta property="og:site_name" content="Bible Strong Avatar Lab" />
<meta property="og:title" content="Bible Strong Avatar Lab — Procedural 2D Avatar Studio" />
<meta
property="og:description"
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
/>
<meta name="twitter:card" content="summary" />
<meta property="og:image" content="https://bible-strong.app/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="Bible Strong Avatar Lab with the blue Strobi procedural avatar"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Bible Strong Avatar Lab" />
<meta
name="twitter:description"
content="Design procedural 2D avatars, expressions and reusable animations directly in your browser."
/>
<meta name="twitter:image" content="https://bible-strong.app/og-image.png" />
<meta
name="twitter:image:alt"
content="Bible Strong Avatar Lab with the blue Strobi procedural avatar"
/>
<link rel="canonical" href="https://bible-strong.app/" />
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
<link rel="manifest" href="./site.webmanifest" />

View File

@ -2,10 +2,19 @@
"name": "bible-strong-avatar-lab",
"version": "0.1.0",
"description": "A browser-based studio for designing procedural 2D avatars, expressions and reusable animations.",
"author": "Stéphane Montlouis-Calixte",
"homepage": "https://bible-strong.app",
"repository": {
"type": "git",
"url": "git+https://github.com/smontlouis/bible-strong-avatar-lab.git"
},
"bugs": {
"url": "https://github.com/smontlouis/bible-strong-avatar-lab/issues"
},
"license": "AGPL-3.0-only",
"private": true,
"type": "module",
"packageManager": "pnpm@11.21.0",
"packageManager": "pnpm@10.34.5",
"engines": {
"node": ">=22.12.0"
},
@ -24,6 +33,8 @@
},
"dependencies": {
"@base-ui/react": "^1.7.0",
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.31.0",

69
pnpm-lock.yaml generated
View File

@ -11,6 +11,12 @@ importers:
'@base-ui/react':
specifier: ^1.7.0
version: 1.7.0(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@vercel/analytics':
specifier: ^2.0.1
version: 2.0.1(react@19.2.3)
'@vercel/speed-insights':
specifier: ^2.0.0
version: 2.0.0(react@19.2.3)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -440,6 +446,61 @@ packages:
'@types/react@19.2.18':
resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==}
'@vercel/analytics@2.0.1':
resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
peerDependencies:
'@remix-run/react': ^2
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
nuxt: '>= 3'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@remix-run/react':
optional: true
'@sveltejs/kit':
optional: true
next:
optional: true
nuxt:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vercel/speed-insights@2.0.0':
resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==}
peerDependencies:
'@sveltejs/kit': ^1 || ^2
next: '>= 13'
nuxt: '>= 3'
react: ^18 || ^19 || ^19.0.0-rc
svelte: '>= 4'
vue: ^3
vue-router: ^4
peerDependenciesMeta:
'@sveltejs/kit':
optional: true
next:
optional: true
nuxt:
optional: true
react:
optional: true
svelte:
optional: true
vue:
optional: true
vue-router:
optional: true
'@vitejs/plugin-react@6.0.5':
resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==}
engines: {node: ^20.19.0 || >=22.12.0}
@ -1298,6 +1359,14 @@ snapshots:
dependencies:
csstype: 3.2.3
'@vercel/analytics@2.0.1(react@19.2.3)':
optionalDependencies:
react: 19.2.3
'@vercel/speed-insights@2.0.0(react@19.2.3)':
optionalDependencies:
react: 19.2.3
'@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.2.4)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.2.1(@types/node@24.13.3)(jiti@2.7.0))':
dependencies:
'@rolldown/pluginutils': 1.0.1

BIN
public/og-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 KiB

View File

@ -1,2 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://bible-strong.app/sitemap.xml

6
public/sitemap.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://bible-strong.app/</loc>
</url>
</urlset>

View File

@ -222,6 +222,32 @@ input {
font-size: 15px;
line-height: 1;
}
.source-link {
display: inline-flex;
align-items: center;
gap: 5px;
color: #f6f7f8;
font-size: 10px;
font-weight: 750;
text-decoration: none;
}
.source-link:hover {
color: white;
}
.source-link:focus-visible {
border-radius: 5px;
outline: 2px solid #7895ee;
outline-offset: 2px;
}
.source-link svg {
width: 14px;
height: 14px;
}
.identity-divider {
align-self: stretch;
width: 1px;
background: rgb(255 255 255 / 14%);
}
.language-picker [data-slot='select-trigger'] {
width: auto;
height: 24px;
@ -1317,7 +1343,7 @@ p {
min-width: 0;
flex-direction: column;
gap: 3px;
border-radius: 12px;
border-radius: 999px;
color: #7b828e;
font-size: 9px;
font-weight: 750;
@ -2386,6 +2412,9 @@ p {
.stage-identity .language-picker [data-slot='select-trigger'] {
min-width: 68px;
}
.stage-identity .source-link > span {
display: none;
}
.avatar-wrap {
width: min(42dvh, 78vw, 340px);
}

View File

@ -1,5 +1,5 @@
import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
import { motion } from 'motion/react'
import { motion, useDragControls } from 'motion/react'
import { Button } from '@/components/ui/button'
import {
@ -9,7 +9,7 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import { DrawerClose, DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer'
import { DrawerContent, DrawerDescription, DrawerTitle } from '@/components/ui/drawer'
import { defaultAvatarEyes } from '@/features/avatar/avatars'
import { ExpressionPreview } from '@/features/avatar/components/ExpressionWorkspace'
import { defaultExpression } from '@/features/avatar/presets'
@ -22,6 +22,7 @@ export function AvatarDrawer({
controller: StudioController
onOpenChange: (open: boolean) => void
}) {
const drawerDragControls = useDragControls()
const {
activateAvatar,
activeAvatarId,
@ -45,10 +46,30 @@ export function AvatarDrawer({
} = controller
return (
<DrawerContent className="avatar-drawer">
<DrawerClose className="drawer-handle" aria-label="Close avatar picker">
<DrawerContent
className="avatar-drawer"
render={
<motion.div
drag="y"
dragControls={drawerDragControls}
dragListener={false}
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.7 }}
dragMomentum={false}
onDragEnd={(_, info) => {
if (info.offset.y > 70 || info.velocity.y > 600) onOpenChange(false)
}}
/>
}
>
<button
className="drawer-handle"
type="button"
aria-label="Close avatar picker"
onPointerDown={event => drawerDragControls.start(event)}
>
<span />
</DrawerClose>
</button>
<div className="avatar-drawer-heading">
<div>
<DrawerTitle>Avatars</DrawerTitle>

View File

@ -1,3 +1,5 @@
import { Code2 } from 'lucide-react'
import {
Select,
SelectContent,
@ -20,6 +22,17 @@ export function StudioIdentity({ className = '', language, setLanguage, t }: Stu
Bible Strong <em>Avatar Lab</em>
</div>
<div className="language-picker">
<a
className="source-link"
href="https://github.com/smontlouis/bible-strong-avatar-lab"
target="_blank"
rel="noreferrer"
aria-label="GitHub"
>
<Code2 aria-hidden="true" />
<span>GitHub</span>
</a>
<span className="identity-divider" aria-hidden="true" />
<span aria-hidden="true">{language === 'en' ? '🇬🇧' : language === 'fr' ? '🇫🇷' : '🇨🇳'}</span>
<Select
value={language}

View File

@ -493,6 +493,7 @@ export function StudioLanguageProvider({ children }: { children: ReactNode }) {
const [language, setLanguage] = useState<StudioLanguage>(readStoredStudioLanguage)
useEffect(() => {
document.documentElement.lang = language
try {
window.localStorage.setItem(STUDIO_LANGUAGE_STORAGE_KEY, language)
} catch {

View File

@ -1,3 +1,5 @@
import { Analytics } from '@vercel/analytics/react'
import { SpeedInsights } from '@vercel/speed-insights/react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
@ -7,5 +9,7 @@ import './app/styles.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<Analytics />
<SpeedInsights />
</StrictMode>
)