Merge pull request 'Release 3.0.3' (#41) from development into main
All checks were successful
Deploy / deploy (push) Successful in 1m14s

Reviewed-on: #41
This commit is contained in:
Leonardo Murça 2026-04-30 16:09:46 +00:00
commit 97477eea0a
22 changed files with 175 additions and 205 deletions

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "3.0.2",
"version": "3.0.3",
"type": "module",
"scripts": {
"dev": "vite dev",

View file

@ -14,23 +14,31 @@ Buy me a coffee and help keep it running ☕"
data-x_margin="18"
data-y_margin="18"
></script>
<!-- Basic -->
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Embroidery Viewer" />
<meta name="theme-color" content="#ffffff" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="canonical" href="https://embroideryviewer.xyz/" />
<!-- Mobile / PWA friendliness -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link
rel="preload"
href="/fonts/merienda.regular.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View file

@ -0,0 +1,89 @@
<script>
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { locale, t } from '$lib/translations';
import { normalizeLocaleUnderscore } from '$lib/utils/normalizeLocaleUnderscore';
import { isDevelopment } from '$lib/utils/env';
/**
* =========================
* Props
* =========================
*/
/** @type {string} Page title (translation key) */
export let title;
/** @type {string} Page description (translation key) */
export let description;
/** @type {string} SEO keywords (translation key or raw string) */
export let keywords;
/** @type {string} Canonical URL (absolute) */
export let url;
/** @type {string} Open Graph type (e.g., 'website', 'article') */
export let ogType = 'website';
/** @type {string} Optional override for Open Graph description */
export let ogDescription = description;
/** @type {string} Twitter card type */
export let twitterCard = 'summary_large_image';
/** @type {boolean} Whether the page should be indexed */
export let shouldBeIndexed = !isDevelopment();
/**
* =========================
* Derived / Computed values
* =========================
*/
let image = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
// Translations (avoid repeating $t everywhere)
$: translatedTitle = title ? $t(title) : '';
$: translatedDescription = description ? $t(description) : '';
$: translatedKeywords = keywords ? $t(keywords) : '';
// Fallbacks
$: finalOgDescription = ogDescription
? $t(ogDescription)
: translatedDescription;
// Locale formatting (e.g., en-US -> en_US)
$: ogLocale = normalizeLocaleUnderscore($locale);
// Robots directive
$: robotsContent = shouldBeIndexed ? 'index, follow' : 'noindex, nofollow';
</script>
<svelte:head>
<!-- Primary SEO -->
<title>{translatedTitle}</title>
<meta name="description" content={translatedDescription} />
<meta name="keywords" content={translatedKeywords} />
<!-- Robots -->
<meta name="robots" content={robotsContent} />
<meta name="googlebot" content={robotsContent} />
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
<meta property="og:type" content={ogType} />
<meta property="og:title" content={translatedTitle} />
<meta property="og:description" content={finalOgDescription} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:locale" content={ogLocale} />
<meta property="og:site_name" content="Embroidery Viewer" />
<!-- Twitter -->
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={translatedTitle} />
<meta name="twitter:description" content={finalOgDescription} />
<meta name="twitter:image" content={image} />
<!-- Optional: improves link previews in some platforms -->
<meta property="og:image:alt" content={translatedTitle} />
</svelte:head>

View file

@ -1,61 +0,0 @@
<script>
import { t } from '$lib/translations';
/** @type {string} Title of the page */
export let title;
/** @type {string} Description of the page */
export let description;
/** @type {string} SEO keywords */
export let keywords;
/** @type {string} Canonical URL of the page */
export let url;
/** @type {string} Main image URL for sharing */
export let image;
/** @type {string} Open Graph type (e.g., 'website', 'article') */
export let ogType = 'website';
/** @type {string} Open Graph title (defaults to title) */
export let ogTitle = title;
/** @type {string} Open Graph description (defaults to description) */
export let ogDescription = description;
/** @type {string} Open Graph image (defaults to image) */
export let ogImage = image;
/** @type {string} Twitter card type (e.g., 'summary_large_image') */
export let twitterCard = 'summary_large_image';
/** @type {string} Twitter title (defaults to title) */
export let twitterTitle = title;
/** @type {string} Twitter description (defaults to description) */
export let twitterDescription = description;
/** @type {string} Twitter image (defaults to image) */
export let twitterImage = image;
</script>
<svelte:head>
<title>{$t(title)}</title>
<meta name="description" content={$t(description)} />
<meta name="keywords" content={$t(keywords)} />
<!-- Open Graph -->
<meta property="og:type" content={ogType} />
<meta property="og:title" content={$t(ogTitle)} />
<meta property="og:description" content={$t(ogDescription)} />
<!-- <meta property="og:image" content={$t(ogImage)} /> -->
<meta property="og:url" content={$t(url)} />
<!-- Twitter -->
<!-- <meta name="twitter:card" content={$t(twitterCard)} /> -->
<meta name="twitter:title" content={$t(twitterTitle)} />
<meta name="twitter:description" content={$t(twitterDescription)} />
<!-- <meta name="twitter:image" content={$t(twitterImage)} /> -->
</svelte:head>

View file

@ -2,6 +2,5 @@
"seo.title": "Free Online Embroidery File Viewer - Fast, Private & No Signup",
"seo.description": "Upload and preview embroidery files instantly with Embroidery Viewer. Supports DST, PES, JEF, EXP, VP3 and more. No installs, no uploads 100% browser-based and free.",
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, VP3 embroidery viewer, embroidery preview tool, browser embroidery renderer, convert embroidery to PNG",
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/"
"seo.url": "https://embroideryviewer.xyz"
}

View file

@ -2,6 +2,5 @@
"seo.title": "Visualizador de Bordado Online Grátis - Rápido, Privado e Sem Cadastro",
"seo.description": "Envie e visualize arquivos de bordado instantaneamente com o Embroidery Viewer. Compatível com DST, PES, JEF, EXP, VP3 e mais. Sem instalações, sem uploads 100% no navegador e gratuito.",
"seo.keywords": "visualizador de bordado, visualizador online de bordado, visualizar arquivos de bordado, visualizar DST, visualizar PES, ferramenta gratuita de bordado, visualizador JEF, bordado EXP, visualizador VP3, pré-visualização de bordado, renderizador de bordado no navegador, converter bordado em PNG",
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/"
"seo.url": "https://embroideryviewer.xyz"
}

View file

@ -0,0 +1,15 @@
/**
* Converts a locale string from hyphen format (e.g., "en-US")
* to underscore format (e.g., "en_US").
*
* Useful for APIs or systems that expect locales with underscores.
*
* @param {string} locale - The locale string in BCP 47 format (e.g., "en-US").
* @returns {string} The normalized locale string using underscores (e.g., "en_US").
*
* @example
* normalizeLocaleUnderscore("en-US"); // "en_US"
*/
export const normalizeLocaleUnderscore = (locale) => {
return locale.split('-').join('_');
};

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'home.seo.title',
description: 'home.seo.description',
keywords: 'home.seo.keywords',
url: 'home.seo.url',
image: 'home.seo.image',
},
};
}

View file

@ -1,17 +1,17 @@
<script>
import Seo from '$lib/components/Seo.svelte';
import Head from '$lib/components/Head.svelte';
import Hero from '$lib/sections/Hero.svelte';
import Features from '$lib/sections/Features.svelte';
import Faq from '$lib/sections/Faq.svelte';
import MobileApp from '$lib/sections/MobileApp.svelte';
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
</script>
<Seo {...metadata} />
<Head
title="home.seo.title"
description="home.seo.description"
keywords="home.seo.keywords"
url="home.seo.url"
/>
<Hero />
<Features />

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'about.seo.title',
description: 'about.seo.description',
keywords: 'about.seo.keywords',
url: 'about.seo.url',
image: 'about.seo.image',
},
};
}

View file

@ -1,24 +1,23 @@
<script>
import DonateIcon from '$lib/components/icons/DonateIcon.svelte';
import { resolve } from '$app/paths';
import { t } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import { isMobile } from '$lib/utils/isMobile';
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
import Head from '$lib/components/Head.svelte';
import DonateIcon from '$lib/components/icons/DonateIcon.svelte';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
</script>
<Seo {...metadata} />
<Head
title="about.seo.title"
description="about.seo.description"
keywords="about.seo.keywords"
url="about.seo.url"
/>
<section aria-labelledby="about-heading">
<div

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'mobile.app.privacy.policy.seo.title',
description: 'mobile.app.privacy.policy.seo.description',
keywords: 'mobile.app.privacy.policy.seo.keywords',
url: 'mobile.app.privacy.policy.seo.url',
image: 'mobile.app.privacy.policy.seo.image',
},
};
}

View file

@ -1,15 +1,14 @@
<script>
import { t } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
import Head from '$lib/components/Head.svelte';
</script>
<Seo {...metadata} />
<Head
title="mobile.app.privacy.policy.seo.title"
description="mobile.app.privacy.policy.seo.description"
keywords="mobile.app.privacy.policy.seo.keywords"
url="mobile.app.privacy.policy.seo.url"
/>
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('mobile.app.privacy.policy.title')}</h1>

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'privacy.policy.seo.title',
description: 'privacy.policy.seo.description',
keywords: 'privacy.policy.seo.keywords',
url: 'privacy.policy.seo.url',
image: 'privacy.policy.seo.image',
},
};
}

View file

@ -1,15 +1,14 @@
<script>
import { t } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
import Head from '$lib/components/Head.svelte';
</script>
<Seo {...metadata} />
<Head
title="privacy.policy.seo.title"
description="privacy.policy.seo.description"
keywords="privacy.policy.seo.keywords"
url="privacy.policy.seo.url"
/>
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('privacy.policy.title')}</h1>

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'support-us.seo.title',
description: 'support-us.seo.description',
keywords: 'support-us.seo.keywords',
url: 'support-us.seo.url',
image: 'support-us.seo.image',
},
};
}

View file

@ -4,20 +4,19 @@
import { isMobile } from '$lib/utils/isMobile';
import BuyMeACoffeeIcon from '$lib/components/icons/BuyMeACoffeeIcon.svelte';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
import Head from '$lib/components/Head.svelte';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
</script>
<Seo {...metadata} />
<Head
title="support-us.seo.title"
description="support-us.seo.description"
keywords="support-us.seo.keywords"
url="support-us.seo.url"
/>
<section aria-labelledby="support-us">
<div

View file

@ -1,12 +0,0 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'terms.of.service.seo.title',
description: 'terms.of.service.seo.description',
keywords: 'terms.of.service.seo.keywords',
url: 'terms.of.service.seo.url',
image: 'terms.of.service.seo.image',
},
};
}

View file

@ -1,15 +1,14 @@
<script>
import { t } from '$lib/translations';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
import Head from '$lib/components/Head.svelte';
</script>
<Seo {...metadata} />
<Head
title="terms.of.service.seo.title"
description="terms.of.service.seo.description"
keywords="terms.of.service.seo.keywords"
url="terms.of.service.seo.url"
/>
<section aria-labelledby="tos-heading">
<h1 id="tos-heading">{$t('terms.of.service.title')}</h1>

View file

@ -6,7 +6,7 @@
import CardList from '$lib/components/CardList.svelte';
import Dropzone from '$lib/components/Dropzone.svelte';
import FileList from '$lib/components/FileList.svelte';
import Seo from '$lib/components/Seo.svelte';
import Head from '$lib/components/Head.svelte';
import { filterFiles } from '$lib/utils/filterFiles';
import { supportedFormats } from '$lib/format-readers';
@ -78,15 +78,14 @@
if (el) el.click();
}
}
/** @type {import('./$types').PageProps} */
let { data } = $props();
// svelte-ignore state_referenced_locally
const metadata = data.metadata;
</script>
<Seo {...metadata} />
<Head
title="viewer.seo.title"
description="viewer.seo.description"
keywords="viewer.seo.keywords"
url="viewer.seo.url"
/>
<!-- eslint-disable svelte/no-at-html-tags -->
<form id="form" enctype="multipart/form-data" onsubmit={onSubmit}>

View file

@ -1 +0,0 @@
google.com, pub-5761689301112420, DIRECT, f08c47fec0942fa0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB