Compare commits

..

6 commits

Author SHA1 Message Date
d6a2af950c Merge pull request 'Small improvements' (#44) from small-improvements into main
Some checks failed
Deploy / deploy (push) Failing after 1m17s
Reviewed-on: #44
2026-05-17 18:39:45 +00:00
b8e1d0f3fe feat: improve structured seo 2026-05-17 15:38:46 -03:00
b019913621 style: add some styling to footer 2026-05-17 15:34:32 -03:00
a00142e17c feat: add custom 404 error page 2026-05-17 15:31:05 -03:00
550b5c3d39 fix: update package-lock.json
All checks were successful
Deploy / deploy (push) Successful in 1m19s
2026-05-17 15:09:19 -03:00
74c2bfd229 Merge pull request 'Create pages to increase organic traffic' (#43) from increase-traffic into main
Some checks failed
Deploy / deploy (push) Failing after 27s
Reviewed-on: #43
2026-05-17 18:05:06 +00:00
11 changed files with 3756 additions and 11 deletions

3194
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,66 @@
</script>
<footer>
<div class="footer-main">
<div class="footer-decoration" aria-hidden="true">
<svg
class="footer-decoration__svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1440 480"
preserveAspectRatio="xMidYMid slice"
fill="none"
>
<g stroke="white" stroke-linecap="round">
<circle
cx="1180"
cy="360"
r="130"
stroke-width="2"
stroke-opacity="0.14"
stroke-dasharray="10 8"
/>
<circle cx="1180" cy="360" r="98" stroke-width="1" stroke-opacity="0.08" />
<path
d="M60 100 Q220 20 400 110 T720 80"
stroke-width="1.5"
stroke-opacity="0.16"
stroke-dasharray="6 10"
/>
<path
d="M100 320 Q340 260 560 340 T980 300"
stroke-width="1.2"
stroke-opacity="0.12"
stroke-dasharray="4 12"
/>
<path
d="M200 60 L240 100 M240 60 L200 100"
stroke-width="1"
stroke-opacity="0.1"
/>
<path
d="M320 400 L350 430 M350 400 L320 430"
stroke-width="1"
stroke-opacity="0.1"
/>
<path
d="M900 140 L930 170 M930 140 L900 170"
stroke-width="1"
stroke-opacity="0.08"
/>
<circle cx="180" cy="200" r="4" fill="white" fill-opacity="0.12" stroke="none" />
<circle cx="210" cy="220" r="3" fill="white" fill-opacity="0.1" stroke="none" />
<circle cx="240" cy="205" r="3.5" fill="white" fill-opacity="0.1" stroke="none" />
<circle cx="680" cy="90" r="3" fill="white" fill-opacity="0.08" stroke="none" />
<circle cx="710" cy="105" r="4" fill="white" fill-opacity="0.08" stroke="none" />
</g>
<path
d="M1320 40 L1348 8 L1356 16 L1328 48 Z"
fill="white"
fill-opacity="0.1"
/>
</svg>
</div>
<div id="content-container">
<section class="footer-block">
<img
@ -55,7 +115,24 @@
>
</section>
</div>
</div>
<section class="credits-container">
<div class="credits-decoration" aria-hidden="true">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1440 48"
preserveAspectRatio="none"
fill="none"
>
<path
d="M0 24 Q180 8 360 24 T720 24 T1080 24 T1440 24"
stroke="white"
stroke-width="1"
stroke-opacity="0.2"
stroke-dasharray="5 9"
/>
</svg>
</div>
Copyright {new Date().getFullYear()}
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
@ -69,11 +146,31 @@
<style>
footer {
background-color: var(--color-primary);
width: 100%;
}
.footer-main {
position: relative;
overflow: hidden;
background-color: var(--color-primary);
}
.footer-decoration {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
.footer-decoration__svg {
width: 100%;
height: 100%;
opacity: 0.95;
}
#content-container {
position: relative;
z-index: 1;
width: 85%;
display: flex;
justify-content: space-between;
@ -154,12 +251,30 @@
}
.credits-container {
position: relative;
z-index: 1;
background-color: var(--color-secondary);
color: white;
margin: 0 auto;
padding: 20px 30px;
padding-left: 9%;
width: 100%;
overflow: hidden;
}
.credits-decoration {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 48px;
pointer-events: none;
transform: translateY(-50%);
}
.credits-decoration svg {
width: 100%;
height: 100%;
}
.credits-container a {

View file

@ -0,0 +1,15 @@
{
"seo.title": "Page not found — Embroidery Viewer",
"seo.description": "The page you are looking for could not be found.",
"seo.keywords": "404, page not found, embroidery viewer",
"seo.url": "https://embroideryviewer.xyz",
"notFound.code": "404",
"notFound.title": "This stitch went off pattern",
"notFound.description": "The page you are looking for may have been moved, removed, or never existed. Let's get you back to previewing embroidery designs.",
"notFound.home": "Back to home",
"notFound.viewer": "Open the viewer",
"notFound.imageAlt": "Embroidery design preview on screen",
"generic.title": "Something went wrong",
"generic.description": "We hit a snag while loading this page. Please try again or return to the home page.",
"generic.home": "Back to home"
}

View file

@ -8,7 +8,7 @@
},
"supportedFormats": {
"summary": "What embroidery file formats are supported?",
"description": "Embroidery Viewer supports popular formats such as PES, DST, and EXP. This allows you to preview most embroidery designs used by home and commercial machines."
"description": "Embroidery Viewer supports PES, DST, JEF, EXP, and PEC — the most common formats for home and commercial embroidery machines."
},
"needSoftware": {
"summary": "Do I need to install any embroidery software?",

View file

@ -1,6 +1,12 @@
{
"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.title": "Free Online Embroidery File Viewer — Fast, Private & No Signup",
"seo.description": "Preview embroidery files instantly in your browser with Embroidery Viewer. Supports PES, DST, JEF, EXP, and PEC. No install, no signup — free and private.",
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, embroidery preview tool, browser embroidery renderer",
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/viewer.png",
"howTo.title": "How to preview embroidery files online",
"howTo.step1": "Open Embroidery Viewer in your web browser.",
"howTo.step2": "Go to the online viewer page.",
"howTo.step3": "Drag and drop your embroidery file (PES, DST, JEF, EXP, or PEC).",
"howTo.step4": "Preview your design instantly — no software installation required."
}

View file

@ -247,6 +247,16 @@ const config = {
key: 'announcement',
loader: async () => (await import('./en-US/announcement.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'error',
loader: async () => (await import('./pt-BR/error.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'error',
loader: async () => (await import('./en-US/error.json')).default,
},
],
};

View file

@ -0,0 +1,15 @@
{
"seo.title": "Página não encontrada — Embroidery Viewer",
"seo.description": "A página que você procura não foi encontrada.",
"seo.keywords": "404, página não encontrada, visualizador de bordado",
"seo.url": "https://embroideryviewer.xyz",
"notFound.code": "404",
"notFound.title": "Este ponto saiu do desenho",
"notFound.description": "A página pode ter sido movida, removida ou nunca existiu. Vamos voltar para visualizar seus bordados.",
"notFound.home": "Voltar ao início",
"notFound.viewer": "Abrir o visualizador",
"notFound.imageAlt": "Prévia de desenho de bordado na tela",
"generic.title": "Algo deu errado",
"generic.description": "Encontramos um problema ao carregar esta página. Tente novamente ou volte ao início.",
"generic.home": "Voltar ao início"
}

View file

@ -8,7 +8,7 @@
},
"supportedFormats": {
"summary": "Quais formatos de arquivos de bordado são suportados?",
"description": "O Embroidery Viewer suporta formatos populares como PES, DST e EXP. Isso permite visualizar a maioria dos designs de bordado usados em máquinas domésticas e industriais."
"description": "O Embroidery Viewer suporta PES, DST, JEF, EXP e PEC — os formatos mais usados em máquinas de bordado domésticas e comerciais."
},
"needSoftware": {
"summary": "Preciso instalar algum software de bordado?",

View file

@ -1,6 +1,12 @@
{
"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.title": "Visualizador de Bordado Online Grátis — Rápido, Privado e Sem Cadastro",
"seo.description": "Visualize arquivos de bordado instantaneamente no navegador com o Embroidery Viewer. Compatível com PES, DST, JEF, EXP e PEC. Sem instalação, sem cadastro — gratuito e privado.",
"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, pré-visualização de bordado, renderizador de bordado no navegador",
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/viewer.png",
"howTo.title": "Como visualizar arquivos de bordado online",
"howTo.step1": "Abra o Embroidery Viewer no seu navegador.",
"howTo.step2": "Acesse a página do visualizador online.",
"howTo.step3": "Arraste e solte seu arquivo de bordado (PES, DST, JEF, EXP ou PEC).",
"howTo.step4": "Visualize o design na hora — sem instalar nenhum software."
}

283
src/routes/+error.svelte Normal file
View file

@ -0,0 +1,283 @@
<script>
import { resolve } from '$app/paths';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import { isMobile } from '$lib/utils/isMobile';
import Head from '$lib/components/Head.svelte';
/** @type {{ status: number }} */
let { status } = $props();
const isNotFound = $derived(status === 404);
const embroideryImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_480/embroidery-viewer/hero-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_640/embroidery-viewer/viewer-screenshot.webp`;
</script>
<Head
title="error.seo.title"
description="error.seo.description"
keywords="error.seo.keywords"
url="error.seo.url"
shouldBeIndexed={false}
/>
<section class="error-page" aria-labelledby="error-heading">
<div class="thread-bg" aria-hidden="true"></div>
<div class="error-card" class:single-column={!isNotFound}>
<div class="copy">
<p class="code">{isNotFound ? $t('error.notFound.code') : status}</p>
<h1 id="error-heading">
{isNotFound ? $t('error.notFound.title') : $t('error.generic.title')}
</h1>
<p class="description">
{isNotFound
? $t('error.notFound.description')
: $t('error.generic.description')}
</p>
<div class="actions">
<a class="organic-btn" href={resolve('/')}>
{isNotFound ? $t('error.notFound.home') : $t('error.generic.home')}
</a>
{#if isNotFound}
<a class="organic-btn-secondary outline" href={resolve('/viewer')}>
{$t('error.notFound.viewer')}
</a>
{/if}
</div>
</div>
{#if isNotFound}
<div class="visual">
<div class="hoop" aria-hidden="true">
<div class="hoop-inner">
<img
src={embroideryImage}
width="640"
height="480"
alt={$t('error.notFound.imageAlt')}
loading="lazy"
/>
</div>
</div>
<div class="needle" aria-hidden="true"></div>
</div>
{/if}
</div>
</section>
<style>
.error-page {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 120px 24px 80px;
overflow: hidden;
background:
radial-gradient(
circle at 15% 20%,
rgba(6, 52, 95, 0.08),
transparent 45%
),
radial-gradient(
circle at 85% 80%,
rgba(25, 71, 149, 0.1),
transparent 50%
),
linear-gradient(180deg, #f8fafb 0%, #eef3f8 100%);
}
.thread-bg {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.35;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' stroke='%2306345f' stroke-width='1.2' opacity='0.5'%3E%3Cpath d='M80 120 Q200 40 320 140 T520 100' stroke-dasharray='6 8'/%3E%3Cpath d='M60 380 Q180 300 300 400 T540 360' stroke-dasharray='4 10'/%3E%3Ccircle cx='300' cy='300' r='120' stroke-dasharray='3 6'/%3E%3C/g%3E%3C/svg%3E");
background-size: 520px;
background-position: center;
background-repeat: no-repeat;
}
.error-card.single-column {
grid-template-columns: 1fr;
max-width: 520px;
text-align: center;
}
.error-card.single-column .description {
margin-left: auto;
margin-right: auto;
}
.error-card.single-column .actions {
justify-content: center;
}
.error-card {
position: relative;
z-index: 1;
width: min(100%, 1000px);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
align-items: center;
padding: 48px;
background: rgba(255, 255, 255, 0.88);
border-radius: 32px 58% 42% 68% / 48% 38% 62% 52%;
box-shadow:
0 24px 48px rgba(6, 52, 95, 0.12),
0 0 0 1px rgba(6, 52, 95, 0.06);
backdrop-filter: blur(6px);
}
.code {
display: inline-block;
margin: 0 0 8px;
font-size: clamp(3rem, 10vw, 4.5rem);
font-weight: 700;
line-height: 1;
letter-spacing: -0.04em;
color: var(--color-primary);
background: linear-gradient(135deg, #06345f 0%, #194795 55%, #3d6eb5 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
h1 {
margin: 0 0 16px;
font-size: clamp(1.5rem, 3vw, 2rem);
line-height: 1.25;
color: var(--color-primary);
}
.description {
margin: 0;
font-size: 1.05rem;
line-height: 1.65;
color: #3a4a5c;
max-width: 38ch;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 14px;
margin-top: 28px;
}
.actions .organic-btn,
.actions .organic-btn-secondary {
font-size: 1rem;
padding: 16px 36px;
}
.organic-btn-secondary.outline {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.organic-btn-secondary.outline:hover {
background: var(--color-primary);
color: white;
}
.visual {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.hoop {
position: relative;
width: min(100%, 340px);
aspect-ratio: 1;
border-radius: 50%;
padding: 14px;
background: linear-gradient(145deg, #c9a227 0%, #8b6914 40%, #d4af37 100%);
box-shadow:
inset 0 2px 4px rgba(255, 255, 255, 0.45),
inset 0 -4px 8px rgba(0, 0, 0, 0.2),
0 16px 32px rgba(6, 52, 95, 0.2);
}
.hoop-inner {
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
background: #f2f6f5;
border: 3px dashed rgba(6, 52, 95, 0.15);
}
.hoop-inner img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.needle {
position: absolute;
top: 8%;
right: 6%;
width: 48px;
height: 48px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%2306345f' d='M8 40 L24 4 L28 8 L14 38 Z'/%3E%3Ccircle cx='8' cy='40' r='4' fill='%23194795'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
transform: rotate(12deg);
filter: drop-shadow(2px 4px 6px rgba(0, 0, 0, 0.15));
}
@media (max-width: 860px) {
.error-card {
grid-template-columns: 1fr;
text-align: center;
padding: 36px 28px;
border-radius: 28px;
}
.description {
margin-left: auto;
margin-right: auto;
}
.actions {
justify-content: center;
}
.visual {
order: -1;
}
.hoop {
width: min(280px, 80vw);
}
}
@media (max-width: 480px) {
.error-page {
padding-top: 100px;
}
.actions {
flex-direction: column;
width: 100%;
}
.actions .organic-btn,
.actions .organic-btn-secondary {
width: 100%;
text-align: center;
}
}
</style>

View file

@ -1,9 +1,107 @@
<script>
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import Head from '$lib/components/Head.svelte';
import StructuredData from '$lib/components/StructuredData.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';
const baseUrl = 'https://embroideryviewer.xyz';
const viewerUrl = `${baseUrl}/viewer`;
const logoUrl = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
const faqKeys = [
'openPesOnline',
'supportedFormats',
'needSoftware',
'isSafe',
'multipleFiles',
'mobileSupport',
];
const howToSteps = ['step1', 'step2', 'step3', 'step4'];
$: structuredData = {
'@context': 'https://schema.org',
'@graph': [
{
'@type': 'WebSite',
'@id': `${baseUrl}/#website`,
url: baseUrl,
name: 'Embroidery Viewer',
description: $t('home.seo.description'),
inLanguage: ['en-US', 'pt-BR'],
publisher: { '@id': `${baseUrl}/#organization` },
},
{
'@type': 'Organization',
'@id': `${baseUrl}/#organization`,
name: 'Embroidery Viewer',
url: baseUrl,
logo: {
'@type': 'ImageObject',
url: logoUrl,
},
email: 'leo@leomurca.xyz',
},
{
'@type': 'WebPage',
'@id': `${baseUrl}/#webpage`,
url: baseUrl,
name: $t('home.seo.title'),
description: $t('home.seo.description'),
isPartOf: { '@id': `${baseUrl}/#website` },
about: { '@id': `${baseUrl}/#webapp` },
primaryImageOfPage: {
'@type': 'ImageObject',
url: $t('home.seo.image'),
},
},
{
'@type': 'WebApplication',
'@id': `${baseUrl}/#webapp`,
name: 'Embroidery Viewer',
url: viewerUrl,
applicationCategory: 'DesignApplication',
operatingSystem: 'Any',
browserRequirements: 'Requires JavaScript. Requires HTML5.',
offers: {
'@type': 'Offer',
price: '0',
priceCurrency: 'USD',
},
description: $t('home.seo.description'),
featureList:
'PES, DST, JEF, EXP, PEC embroidery file preview; multiple files; browser-based',
screenshot: $t('home.seo.image'),
},
{
'@type': 'FAQPage',
'@id': `${baseUrl}/#faq`,
mainEntity: faqKeys.map((key) => ({
'@type': 'Question',
name: $t(`faq.items.${key}.summary`),
acceptedAnswer: {
'@type': 'Answer',
text: $t(`faq.items.${key}.description`),
},
})),
},
{
'@type': 'HowTo',
'@id': `${baseUrl}/#howto`,
name: $t('home.howTo.title'),
step: howToSteps.map((step, i) => ({
'@type': 'HowToStep',
position: i + 1,
text: $t(`home.howTo.${step}`),
})),
},
],
};
</script>
<Head
@ -11,8 +109,11 @@
description="home.seo.description"
keywords="home.seo.keywords"
url="home.seo.url"
ogImage="home.seo.image"
/>
<StructuredData data={structuredData} />
<Hero />
<Features />
<MobileApp />