feat: sitio hostingdelsur.net v2 con Astro 5, Tailwind v4, soporte light/dark, i18n es/en, Keystatic CMS, reCAPTCHA v3
- Arista Pro Alternate Regular self-hosted (font corporativa) - Toggle theme con CSS variables y @custom-variant dark - 6 servicios en 3 categorías (Hosting & Correo / Diseño & Contenido / Infraestructura) - 3 planes destacados (Básico USD 59, Institucional USD 129, E-commerce USD 219) - Datacenters en 4 países (Canadá, USA, Alemania, Uruguay) sin ciudades en el sitio - Sede operativa en Maldonado, Uruguay - i18n es/en con contenido duplicado en Keystatic - Endpoint PHP para form de contacto con PHPMailer + reCAPTCHA v3 + honeypot + rate limit - WorldMap con animación SVG de los 4 países - 29 páginas generadas, 0 JS por default - Sitemap auto + robots.txt - JSON-LD Organization + ProfessionalService con areaServed
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
---
|
||||
const label = 'Volver arriba';
|
||||
---
|
||||
<button
|
||||
type="button"
|
||||
id="back-to-top"
|
||||
class="fixed bottom-24 right-6 z-30 inline-flex items-center justify-center w-11 h-11 rounded-full text-white shadow-md opacity-0 invisible transition-all"
|
||||
style="background: var(--hds-fg);"
|
||||
aria-label={label}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<polyline points="18 15 12 9 6 15"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
const btn = document.getElementById('back-to-top');
|
||||
if (!btn) return;
|
||||
let visible = false;
|
||||
function update() {
|
||||
const should = window.scrollY > 400;
|
||||
if (should !== visible) {
|
||||
visible = should;
|
||||
if (should) {
|
||||
btn.classList.remove('opacity-0', 'invisible');
|
||||
btn.classList.add('opacity-100', 'visible');
|
||||
} else {
|
||||
btn.classList.remove('opacity-100', 'visible');
|
||||
btn.classList.add('opacity-0', 'invisible');
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('scroll', update, { passive: true });
|
||||
btn.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
interface Props {
|
||||
variant?: 'wave' | 'cloud';
|
||||
flip?: boolean;
|
||||
fillClass?: string;
|
||||
}
|
||||
const { variant = 'wave', flip = false, fillClass = 'fill-hds-bg' } = Astro.props;
|
||||
---
|
||||
{variant === 'wave' ? (
|
||||
<div class={`w-full overflow-hidden leading-none ${flip ? 'rotate-180' : ''}`} aria-hidden="true">
|
||||
<svg class="w-full h-12 md:h-16" viewBox="0 0 1440 80" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class={fillClass} d="M0,32 C240,80 480,0 720,32 C960,64 1200,16 1440,48 L1440,80 L0,80 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div class={`w-full overflow-hidden leading-none ${flip ? 'rotate-180' : ''}`} aria-hidden="true">
|
||||
<svg class="w-full h-16 md:h-24" viewBox="0 0 1440 120" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path class={fillClass} d="M0,60 C120,100 240,20 360,50 C480,80 600,30 720,55 C840,80 960,40 1080,60 C1200,80 1320,45 1440,65 L1440,120 L0,120 Z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import SectionEyebrow from './SectionEyebrow.astro';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
whatsappUrl: string;
|
||||
}
|
||||
const { t, whatsappUrl } = Astro.props;
|
||||
---
|
||||
<section class="relative overflow-hidden py-20 md:py-32" style="background: #0F0E0C; color: #FAF6EE;">
|
||||
<div class="absolute inset-0 particle-grid" aria-hidden="true" style="opacity: 0.7;"></div>
|
||||
|
||||
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="inline-block">
|
||||
<span class="inline-flex items-center gap-3 text-xs font-semibold tracking-[0.25em] uppercase" style="color: #FA9F5C;">
|
||||
<span class="inline-block w-10 h-px" style="background: #FA9F5C;"></span>
|
||||
{t('ctaFinal.eyebrow') || 'Hablemos'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 class="font-display leading-tight mt-5 mb-5" style="font-size: clamp(2.25rem, 5vw, 4rem); color: #FAF6EE;">
|
||||
{t('ctaFinal.title')}
|
||||
</h2>
|
||||
<p class="text-lg md:text-xl max-w-2xl mx-auto mb-10" style="color: #C8C2B8;">
|
||||
{t('ctaFinal.body')}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-primary text-base">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
|
||||
</svg>
|
||||
{t('ctaFinal.button')}
|
||||
</a>
|
||||
<a href="mailto:contacto@hostingdelsur.net" class="inline-flex items-center justify-center gap-2 px-7 py-3.5 rounded-md text-base font-semibold transition-colors" style="border: 2px solid #FAF6EE; color: #FAF6EE; background: transparent;" onmouseover="this.style.background='#FAF6EE'; this.style.color='#0F0E0C';" onmouseout="this.style.background='transparent'; this.style.color='#FAF6EE';">
|
||||
contacto@hostingdelsur.net
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
import SectionEyebrow from './SectionEyebrow.astro';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
import MateIcon from './icons/MateIcon.astro';
|
||||
import SovereigntyIcon from './icons/SovereigntyIcon.astro';
|
||||
import GlobeIcon from './icons/GlobeIcon.astro';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
}
|
||||
const { t } = Astro.props;
|
||||
|
||||
const items = [
|
||||
{ Icon: MateIcon, titleKey: 'diff.support.title' as TranslationKey, bodyKey: 'diff.support.body' as TranslationKey },
|
||||
{ Icon: SovereigntyIcon, titleKey: 'diff.sovereignty.title' as TranslationKey, bodyKey: 'diff.sovereignty.body' as TranslationKey },
|
||||
{ Icon: GlobeIcon, titleKey: 'diff.infra.title' as TranslationKey, bodyKey: 'diff.infra.body' as TranslationKey },
|
||||
];
|
||||
---
|
||||
<section class="py-20 md:py-28" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-3xl mb-14 md:mb-20">
|
||||
<SectionEyebrow text={t('diff.eyebrow')} />
|
||||
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 5vw, 3.5rem); color: var(--hds-fg);">
|
||||
{t('diff.title')}
|
||||
</h2>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{t('diff.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{items.map((item) => (
|
||||
<article class="service-card">
|
||||
<div class="icon-circle mb-6">
|
||||
<item.Icon class="w-7 h-7" />
|
||||
</div>
|
||||
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);">
|
||||
{t(item.titleKey)}
|
||||
</h3>
|
||||
<p class="text-[15px] leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||
{t(item.bodyKey)}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
interface FaqItem {
|
||||
q: string;
|
||||
a: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
items: FaqItem[];
|
||||
}
|
||||
const { items } = Astro.props;
|
||||
|
||||
const groupId = `faq-${Math.random().toString(36).slice(2, 8)}`;
|
||||
---
|
||||
<div class="divide-y" style="border-color: var(--hds-line);">
|
||||
{items.map((item, idx) => {
|
||||
const inputId = `${groupId}-${idx}`;
|
||||
return (
|
||||
<details class="group" style="border-color: var(--hds-line);">
|
||||
<summary
|
||||
class="flex items-start justify-between gap-4 py-5 cursor-pointer list-none transition-colors"
|
||||
style="color: var(--hds-fg);"
|
||||
>
|
||||
<h3 class="text-base md:text-lg font-semibold pr-4">{item.q}</h3>
|
||||
<span class="flex-shrink-0 mt-1 icon-circle transition-transform group-open:rotate-45" style="width: 28px; height: 28px;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="pb-5 pr-12 text-[15px] leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||
{item.a}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
import type { Lang } from '@/i18n/utils';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const { t, lang } = Astro.props;
|
||||
const year = new Date().getFullYear();
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
|
||||
const navLinks: { href: string; key: TranslationKey }[] = [
|
||||
{ href: `${base}/`, key: 'nav.home' },
|
||||
{ href: `${base}/servicios/`, key: 'nav.services' },
|
||||
{ href: `${base}/planes/`, key: 'nav.plans' },
|
||||
{ href: `${base}/nosotros/`, key: 'nav.about' },
|
||||
{ href: `${base}/instructivos/`, key: 'nav.tutorials' },
|
||||
{ href: `${base}/contacto/`, key: 'nav.contact' },
|
||||
];
|
||||
|
||||
const legalLinks = [
|
||||
{ href: `${base}/legal/privacidad/`, key: 'footer.privacy' as TranslationKey },
|
||||
{ href: `${base}/legal/terminos/`, key: 'footer.terms' as TranslationKey },
|
||||
];
|
||||
---
|
||||
<footer class="py-14 md:py-20" style="background: #0F0E0C; color: #FAF6EE;">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-10 md:gap-8">
|
||||
<div class="md:col-span-4">
|
||||
<a href={lang === 'en' ? '/en/' : '/'} class="inline-flex items-center gap-2.5 mb-4" aria-label="Hosting del Sur">
|
||||
<img src="/logo/hds-mark.svg" alt="" width="36" height="36" class="h-9 w-9" aria-hidden="true" style="filter: brightness(0) invert(1);" />
|
||||
<span class="text-lg font-display tracking-tight">
|
||||
<span style="color: #EE7623;">hosting</span> del <span style="color: #EE7623;">sur</span>
|
||||
</span>
|
||||
</a>
|
||||
<p class="text-sm leading-relaxed max-w-sm" style="color: #C8C2B8;">
|
||||
{t('footer.tagline')}
|
||||
</p>
|
||||
<p class="text-xs mt-4" style="color: #8A8580;">
|
||||
{lang === 'en' ? 'Web, email, infrastructure and human support in Uruguay.' : 'Web, correo, infraestructura y soporte humano en Uruguay.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-4" style="color: #FA9F5C;">
|
||||
{t('footer.nav')}
|
||||
</h2>
|
||||
<ul class="space-y-2.5">
|
||||
{navLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm transition-colors hover:text-white"
|
||||
style="color: #C8C2B8;"
|
||||
>
|
||||
{t(link.key)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-4" style="color: #FA9F5C;">
|
||||
{t('footer.contact')}
|
||||
</h2>
|
||||
<ul class="space-y-2.5 text-sm" style="color: #C8C2B8;">
|
||||
<li>
|
||||
<a href="https://wa.me/59899812487" target="_blank" rel="noopener noreferrer" class="hover:text-white transition-colors">
|
||||
+598 99 812 487
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="mailto:contacto@hostingdelsur.net" class="hover:text-white transition-colors break-all">
|
||||
contacto@hostingdelsur.net
|
||||
</a>
|
||||
</li>
|
||||
<li>Maldonado, Uruguay</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="md:col-span-3">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-4" style="color: #FA9F5C;">
|
||||
{t('footer.legal')}
|
||||
</h2>
|
||||
<ul class="space-y-2.5">
|
||||
{legalLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.href}
|
||||
class="text-sm transition-colors hover:text-white"
|
||||
style="color: #C8C2B8;"
|
||||
>
|
||||
{t(link.key)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 pt-6 border-t flex flex-col sm:flex-row sm:justify-between gap-3 text-xs" style="border-color: #2A2520; color: #8A8580;">
|
||||
<p>© {year} Hosting del Sur. {t('footer.copyright')}</p>
|
||||
<p>Maldonado, Uruguay</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
whatsappUrl: string;
|
||||
}
|
||||
|
||||
const { t, whatsappUrl } = Astro.props;
|
||||
---
|
||||
<section class="relative overflow-hidden pt-24 pb-32 md:pt-32 md:pb-40">
|
||||
<div class="absolute inset-0 particle-grid" aria-hidden="true"></div>
|
||||
<div class="absolute inset-0" style="background: linear-gradient(180deg, transparent 0%, var(--hds-bg) 100%);" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="eyebrow mb-7">
|
||||
<span>Premium hosting · Desde 2000</span>
|
||||
</div>
|
||||
|
||||
<h1 class="font-display leading-[0.98] mb-8" style="font-size: clamp(2.75rem, 7vw, 5.5rem); color: var(--hds-fg); letter-spacing: -0.025em; font-weight: 400;">
|
||||
Alojamiento en la nube<br/>
|
||||
que te <span style="color: var(--color-hds-naranja); font-style: italic;">conoce</span>.
|
||||
</h1>
|
||||
|
||||
<p class="text-lg md:text-xl leading-relaxed mb-10 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{t('hero.body')}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 mb-14">
|
||||
<a href="#planes" class="btn-primary">
|
||||
{t('hero.ctaPrimary')}
|
||||
</a>
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-secondary">
|
||||
{t('hero.ctaSecondary')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-8 gap-y-3 text-sm" style="color: var(--hds-fg-muted);">
|
||||
<span class="flex items-center gap-2">
|
||||
<span style="color: var(--color-hds-naranja);">●</span>
|
||||
26+ años
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span style="color: var(--color-hds-naranja);">●</span>
|
||||
Uptime 99.9%
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span style="color: var(--color-hds-naranja);">●</span>
|
||||
{t('diff.support.title')}
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span style="color: var(--color-hds-naranja);">●</span>
|
||||
Maldonado, Uruguay
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
import { LANGUAGES, DEFAULT_LANG, type Lang } from '@/i18n/utils';
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
}
|
||||
const { lang } = Astro.props;
|
||||
const path = Astro.url.pathname;
|
||||
|
||||
function otherLangUrl(): string {
|
||||
const cleanPath = path.replace(/^\/(en|es)/, '') || '/';
|
||||
if (lang === DEFAULT_LANG) {
|
||||
return `/en${cleanPath === '/' ? '' : cleanPath}`;
|
||||
}
|
||||
return cleanPath;
|
||||
}
|
||||
|
||||
const other = LANGUAGES.find((l) => l.code !== lang);
|
||||
const isEn = lang === 'en';
|
||||
---
|
||||
<a
|
||||
href={otherLangUrl()}
|
||||
hreflang={other?.code}
|
||||
class="text-xs font-semibold tracking-wider px-2.5 py-1.5 rounded border transition-colors"
|
||||
style="border-color: var(--hds-line); color: var(--hds-fg-soft);"
|
||||
aria-label={isEn ? 'Switch language to Spanish' : 'Cambiar idioma a inglés'}
|
||||
onmouseover="this.style.borderColor='var(--color-hds-naranja)';this.style.color='var(--color-hds-naranja)';"
|
||||
onmouseout="this.style.borderColor='var(--hds-line)';this.style.color='var(--hds-fg-soft)';"
|
||||
>
|
||||
{other?.label}
|
||||
</a>
|
||||
@@ -0,0 +1,143 @@
|
||||
---
|
||||
import ThemeToggle from './ThemeToggle.astro';
|
||||
import LangToggle from './LangToggle.astro';
|
||||
import { type Lang } from '@/i18n/utils';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
lang: Lang;
|
||||
currentPath?: string;
|
||||
}
|
||||
|
||||
const { t, lang, currentPath } = Astro.props;
|
||||
const path = currentPath || Astro.url.pathname;
|
||||
|
||||
const navItems: { label: string; href: string; key: TranslationKey }[] = [
|
||||
{ label: t('nav.home'), href: lang === 'en' ? '/en/' : '/', key: 'nav.home' },
|
||||
{ label: t('nav.services'), href: lang === 'en' ? '/en/servicios/' : '/servicios/', key: 'nav.services' },
|
||||
{ label: t('nav.plans'), href: lang === 'en' ? '/en/planes/' : '/planes/', key: 'nav.plans' },
|
||||
{ label: t('nav.about'), href: lang === 'en' ? '/en/nosotros/' : '/nosotros/', key: 'nav.about' },
|
||||
{ label: t('nav.tutorials'), href: lang === 'en' ? '/en/instructivos/' : '/instructivos/', key: 'nav.tutorials' },
|
||||
{ label: t('nav.contact'), href: lang === 'en' ? '/en/contacto/' : '/contacto/', key: 'nav.contact' },
|
||||
];
|
||||
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
const isActive = (href: string) => {
|
||||
const normalized = path.replace(/\/$/, '') || '/';
|
||||
const target = href.replace(/\/$/, '') || '/';
|
||||
return normalized === target;
|
||||
};
|
||||
---
|
||||
<header id="navbar" class="sticky top-0 z-40 backdrop-blur-md border-b transition-colors" style="background: rgba(250,246,238,0.85); border-color: var(--hds-line);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16 md:h-20">
|
||||
<a href={lang === 'en' ? '/en/' : '/'} class="flex items-center gap-2.5 flex-shrink-0" aria-label="Hosting del Sur - Inicio">
|
||||
<img
|
||||
src="/logo/hds-mark.svg"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
class="h-8 w-8"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="text-base font-display tracking-tight" style="color: var(--hds-fg);">
|
||||
<span style="color: var(--color-hds-naranja);">hosting</span> del <span style="color: var(--color-hds-naranja);">sur</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="hidden lg:flex items-center gap-7" aria-label="Principal">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class:list={[
|
||||
"text-sm font-medium transition-colors",
|
||||
isActive(item.href) ? "text-hds-naranja" : ""
|
||||
]}
|
||||
style={isActive(item.href) ? 'color: var(--color-hds-naranja);' : 'color: var(--hds-fg-soft);'}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<LangToggle lang={lang} />
|
||||
<ThemeToggle lang={lang} />
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hidden md:inline-flex btn-primary text-sm"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
|
||||
</svg>
|
||||
<span>{t('nav.cta')}</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="lg:hidden p-2 -mr-2"
|
||||
aria-label="Abrir menú"
|
||||
aria-expanded="false"
|
||||
aria-controls="mobile-menu"
|
||||
id="mobile-menu-button"
|
||||
style="color: var(--hds-fg);"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="3" y1="6" x2="21" y2="6"/>
|
||||
<line x1="3" y1="12" x2="21" y2="12"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mobile-menu" class="lg:hidden hidden border-t py-4" style="border-color: var(--hds-line);">
|
||||
<nav class="flex flex-col gap-3" aria-label="Móvil">
|
||||
{navItems.map((item) => (
|
||||
<a
|
||||
href={item.href}
|
||||
class="text-base font-medium py-2"
|
||||
style={isActive(item.href) ? 'color: var(--color-hds-naranja);' : 'color: var(--hds-fg-soft);'}
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="md:hidden inline-flex items-center justify-center gap-2 btn-primary text-sm mt-2"
|
||||
>
|
||||
<span>{t('nav.cta')}</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
const btn = document.getElementById('mobile-menu-button');
|
||||
const menu = document.getElementById('mobile-menu');
|
||||
if (!btn || !menu) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const isOpen = !menu.classList.contains('hidden');
|
||||
if (isOpen) {
|
||||
menu.classList.add('hidden');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
menu.classList.remove('hidden');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(:root.dark) #navbar {
|
||||
background: rgba(26,22,18,0.85) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
opacity?: number;
|
||||
}
|
||||
const { size = 'md', opacity = 0.10 } = Astro.props;
|
||||
|
||||
const sizes = {
|
||||
sm: 40,
|
||||
md: 56,
|
||||
lg: 72,
|
||||
};
|
||||
const cell = sizes[size];
|
||||
---
|
||||
<div
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
aria-hidden="true"
|
||||
style={`background-image: linear-gradient(to right, rgba(238,118,35,${opacity}) 1px, transparent 1px), linear-gradient(to bottom, rgba(238,118,35,${opacity}) 1px, transparent 1px); background-size: ${cell}px ${cell}px;`}
|
||||
></div>
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
interface Props {
|
||||
plans: Array<{
|
||||
slug: string;
|
||||
name: string;
|
||||
priceUsdYear: number;
|
||||
recommended?: boolean;
|
||||
features: string[];
|
||||
}>;
|
||||
whatsappUrl: string;
|
||||
additionalNote?: string;
|
||||
}
|
||||
|
||||
const { plans, whatsappUrl, additionalNote } = Astro.props;
|
||||
|
||||
function monthlyEquivalent(annual: number): string {
|
||||
return (annual / 12).toFixed(2);
|
||||
}
|
||||
---
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{plans.map((plan) => (
|
||||
<div
|
||||
class="relative rounded-2xl p-8 transition-all flex flex-col h-full"
|
||||
style={plan.recommended
|
||||
? `background: var(--hds-card); border: 2px solid var(--color-hds-naranja); box-shadow: 0 16px 40px rgba(238,118,35,0.15);`
|
||||
: `background: var(--hds-card); border: 1px solid var(--hds-line);`}
|
||||
>
|
||||
{plan.recommended && (
|
||||
<div class="absolute -top-3.5 left-1/2 -translate-x-1/2 text-[11px] font-bold tracking-[0.2em] uppercase px-4 py-1.5 rounded-full whitespace-nowrap" style="background: var(--color-hds-naranja); color: white; box-shadow: 0 4px 12px rgba(238,118,35,0.3);">
|
||||
⭐ Recomendado
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mb-5">
|
||||
<div class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style={`color: ${plan.recommended ? 'var(--color-hds-naranja)' : 'var(--hds-fg-muted)'};`}>
|
||||
{plan.name}
|
||||
</div>
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-2xl font-semibold" style="color: var(--hds-fg);">USD</span>
|
||||
<span class="font-display leading-none" style="font-size: 4rem; color: var(--hds-fg); font-weight: 500; letter-spacing: -0.02em;">{plan.priceUsdYear}</span>
|
||||
</div>
|
||||
<div class="text-sm mt-2" style="color: var(--hds-fg-muted);">
|
||||
por año · ~USD {monthlyEquivalent(plan.priceUsdYear)}/mes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-6 mb-6 flex-grow" style="border-color: var(--hds-line);">
|
||||
<ul class="space-y-2.5 text-sm" style="color: var(--hds-fg-soft);">
|
||||
{plan.features.map((feature) => (
|
||||
<li class="flex items-start gap-2.5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" style="color: var(--color-hds-naranja);" aria-hidden="true">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={plan.recommended ? 'btn-primary justify-center' : 'btn-secondary justify-center'}
|
||||
>
|
||||
Elegir {plan.name}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{additionalNote && (
|
||||
<p class="text-center text-sm mt-6 italic" style="color: var(--hds-fg-muted);">
|
||||
{additionalNote}
|
||||
</p>
|
||||
)}
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
import SectionEyebrow from './SectionEyebrow.astro';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
}
|
||||
const { t } = Astro.props;
|
||||
|
||||
const steps = [
|
||||
{ num: '01', titleKey: 'process.step1.title' as TranslationKey, bodyKey: 'process.step1.body' as TranslationKey },
|
||||
{ num: '02', titleKey: 'process.step2.title' as TranslationKey, bodyKey: 'process.step2.body' as TranslationKey },
|
||||
{ num: '03', titleKey: 'process.step3.title' as TranslationKey, bodyKey: 'process.step3.body' as TranslationKey },
|
||||
{ num: '04', titleKey: 'process.step4.title' as TranslationKey, bodyKey: 'process.step4.body' as TranslationKey },
|
||||
];
|
||||
---
|
||||
<section class="py-20 md:py-28" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-3xl mb-14 md:mb-20">
|
||||
<SectionEyebrow text={t('process.eyebrow')} />
|
||||
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 5vw, 3.5rem); color: var(--hds-fg);">
|
||||
{t('process.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-10 md:gap-8 relative">
|
||||
<div class="hidden lg:block absolute top-12 left-[12.5%] right-[12.5%] h-px" style="background: linear-gradient(to right, transparent, var(--color-hds-naranja) 20%, var(--color-hds-naranja) 80%, transparent);" aria-hidden="true"></div>
|
||||
|
||||
{steps.map((step) => (
|
||||
<li class="relative">
|
||||
<div class="font-display leading-none mb-4 relative inline-block" style="font-size: 5rem; color: var(--color-hds-naranja); font-weight: 500; opacity: 0.85;" aria-hidden="true">
|
||||
{step.num}
|
||||
<span class="absolute -bottom-1 left-0 w-10 h-1" style="background: var(--color-hds-naranja);"></span>
|
||||
</div>
|
||||
<h3 class="text-xl md:text-2xl font-semibold mb-3" style="color: var(--hds-fg);">
|
||||
{t(step.titleKey)}
|
||||
</h3>
|
||||
<p class="text-[15px] leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||
{t(step.bodyKey)}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
interface Props {
|
||||
text: string;
|
||||
align?: 'left' | 'center';
|
||||
light?: boolean;
|
||||
}
|
||||
const { text, align = 'left', light = false } = Astro.props;
|
||||
---
|
||||
{align === 'center' ? (
|
||||
<div class="text-center">
|
||||
<span class="inline-flex items-center gap-3 text-xs font-semibold tracking-[0.25em] uppercase" style={`color: ${light ? '#FA9F5C' : 'var(--color-hds-naranja)'}`}>
|
||||
<span class="inline-block w-10 h-px" style={`background: ${light ? '#FA9F5C' : 'var(--color-hds-naranja)'}`}></span>
|
||||
{text}
|
||||
<span class="inline-block w-10 h-px" style={`background: ${light ? '#FA9F5C' : 'var(--color-hds-naranja)'}`}></span>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div class="inline-flex items-center gap-3 text-xs font-semibold tracking-[0.25em] uppercase" style={`color: ${light ? '#FA9F5C' : 'var(--color-hds-naranja)'}`}>
|
||||
{text}
|
||||
<span class="inline-block w-12 h-px" style={`background: ${light ? '#FA9F5C' : 'var(--color-hds-naranja)'}`}></span>
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
Icon: any;
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
const { href, title, description, Icon, borderColor = 'var(--color-hds-naranja)' } = Astro.props;
|
||||
---
|
||||
<a
|
||||
href={href}
|
||||
class="service-card group flex flex-col h-full"
|
||||
style={`border-top: 4px solid ${borderColor}; padding-top: 1.5rem;`}
|
||||
>
|
||||
<div class="icon-circle mb-5" style="width: 48px; height: 48px;">
|
||||
<Icon class="w-6 h-6" />
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold mb-2 transition-colors" style="color: var(--hds-fg);">
|
||||
{title}
|
||||
</h3>
|
||||
<p class="text-sm leading-relaxed mb-4 flex-grow" style="color: var(--hds-fg-soft);">
|
||||
{description}
|
||||
</p>
|
||||
<span class="btn-ghost mt-auto">
|
||||
Ver detalles
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
import ServiceCard from './ServiceCard.astro';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
|
||||
interface Service {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
Icon: any;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
categoryTitle: string;
|
||||
categoryKey: TranslationKey;
|
||||
services: Service[];
|
||||
lang: 'es' | 'en';
|
||||
}
|
||||
|
||||
const { categoryTitle, categoryKey, services, lang } = Astro.props;
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
---
|
||||
<div class="mb-12 last:mb-0">
|
||||
<div class="flex items-baseline justify-between mb-6">
|
||||
<h3 class="text-2xl md:text-3xl text-hds-tinta">
|
||||
{categoryTitle}
|
||||
</h3>
|
||||
<span class="text-xs font-semibold tracking-wider uppercase text-hds-coral">
|
||||
{categoryKey}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
|
||||
{services.map((service) => (
|
||||
<ServiceCard
|
||||
href={`${base}/servicios/${service.slug}/`}
|
||||
title={service.title}
|
||||
description={service.description}
|
||||
Icon={service.Icon}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,140 @@
|
||||
---
|
||||
import ServiceCard from './ServiceCard.astro';
|
||||
import SectionEyebrow from './SectionEyebrow.astro';
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
import MailIcon from './icons/MailIcon.astro';
|
||||
import WordpressIcon from './icons/WordpressIcon.astro';
|
||||
import CodeIcon from './icons/CodeIcon.astro';
|
||||
import ShopIcon from './icons/ShopIcon.astro';
|
||||
import TranslateIcon from './icons/TranslateIcon.astro';
|
||||
import ServerIcon from './icons/ServerIcon.astro';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
lang: 'es' | 'en';
|
||||
}
|
||||
|
||||
const { t, lang } = Astro.props;
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
|
||||
const groups = [
|
||||
{
|
||||
title: t('services.cat.hosting'),
|
||||
items: [
|
||||
{
|
||||
slug: 'hosting-correo-web',
|
||||
title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web',
|
||||
desc: lang === 'en'
|
||||
? 'Optimized hosting with cPanel for WordPress, PHP and MySQL.'
|
||||
: 'Alojamiento optimizado con cPanel para WordPress, PHP y MySQL.',
|
||||
Icon: MailIcon,
|
||||
border: 'var(--color-hds-naranja)',
|
||||
},
|
||||
{
|
||||
slug: 'mantenimiento-wordpress',
|
||||
title: lang === 'en' ? 'WordPress maintenance' : 'Mantenimiento WordPress',
|
||||
desc: lang === 'en'
|
||||
? 'Security patches and updates by official WordPress ecosystem experts.'
|
||||
: 'Parches de seguridad y actualizaciones por expertos del ecosistema WP.',
|
||||
Icon: WordpressIcon,
|
||||
border: 'var(--color-hds-naranja)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.design'),
|
||||
items: [
|
||||
{
|
||||
slug: 'desarrollo-webs-institucionales',
|
||||
title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales',
|
||||
desc: lang === 'en'
|
||||
? 'Corporate sites tailored to your business and identity.'
|
||||
: 'Sitios corporativos a medida de tu negocio e identidad.',
|
||||
Icon: CodeIcon,
|
||||
border: 'var(--color-hds-coral)',
|
||||
},
|
||||
{
|
||||
slug: 'e-commerce',
|
||||
title: lang === 'en' ? 'E-commerce websites' : 'Sitios web con e-commerce',
|
||||
desc: lang === 'en'
|
||||
? 'WooCommerce stores with secure payments and inventory.'
|
||||
: 'Tiendas WooCommerce con pagos seguros y gestión de stock.',
|
||||
Icon: ShopIcon,
|
||||
border: 'var(--color-hds-coral)',
|
||||
},
|
||||
{
|
||||
slug: 'gestion-contenido-traducciones',
|
||||
title: lang === 'en' ? 'Content & translations' : 'Gestión de contenido y traducciones',
|
||||
desc: lang === 'en'
|
||||
? 'Recurring editorial: copy, updates, blog and translations.'
|
||||
: 'Servicio editorial recurrente: redacción, blog, traducciones.',
|
||||
Icon: TranslateIcon,
|
||||
border: 'var(--color-hds-coral)',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.infra'),
|
||||
items: [
|
||||
{
|
||||
slug: 'servidores-linux-virtualizacion',
|
||||
title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización',
|
||||
desc: lang === 'en'
|
||||
? 'Linux servers, KVM virtualization, private clouds.'
|
||||
: 'Servidores Linux, virtualización KVM, nubes privadas.',
|
||||
Icon: ServerIcon,
|
||||
border: 'var(--color-hds-durazno)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
<section id="servicios" class="py-20 md:py-28" style="background: var(--hds-bg);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-3xl mb-14 md:mb-20">
|
||||
<SectionEyebrow text={t('services.eyebrow')} />
|
||||
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 5vw, 3.5rem); color: var(--hds-fg);">
|
||||
{t('services.title')}
|
||||
</h2>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{t('services.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-14">
|
||||
{groups.map((group) => (
|
||||
<div>
|
||||
<div class="flex items-baseline gap-4 mb-6">
|
||||
<h3 class="text-2xl md:text-3xl font-display" style="color: var(--hds-fg);">
|
||||
{group.title}
|
||||
</h3>
|
||||
<span class="text-xs font-semibold tracking-[0.25em] uppercase" style="color: var(--color-hds-naranja);">
|
||||
{group.items.length} {group.items.length === 1 ? 'servicio' : 'servicios'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{group.items.map((item) => (
|
||||
<ServiceCard
|
||||
href={`${base}/servicios/${item.slug}/`}
|
||||
title={item.title}
|
||||
description={item.desc}
|
||||
Icon={item.Icon}
|
||||
borderColor={item.border}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-14">
|
||||
<a href={`${base}/servicios/`} class="btn-secondary">
|
||||
Ver todos los servicios
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
interface Props {
|
||||
size?: number;
|
||||
duration?: number;
|
||||
suffix?: string;
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
light?: boolean;
|
||||
}
|
||||
|
||||
const { size = 96, duration = 2000, suffix = '', label, sublabel, light = false } = Astro.props;
|
||||
const numId = `stat-${Math.random().toString(36).slice(2, 9)}`;
|
||||
---
|
||||
<div class="text-center group">
|
||||
<div class={`stat-number font-display leading-none ${light ? 'text-white' : ''}`} style={`font-size: ${size}px; color: var(--color-hds-naranja); font-weight: 500;`} data-target={typeof label === 'string' && /^\d+$/.test(label) ? label : '0'} data-suffix={suffix} data-duration={duration} id={numId}>
|
||||
{label}{suffix}
|
||||
</div>
|
||||
<div class={`text-sm font-semibold mt-3 ${light ? 'text-hds-crema-soft' : ''}`} style={light ? 'color: #FAF6EE' : 'color: var(--hds-fg-soft)'}>
|
||||
<slot name="label">{label}</slot>
|
||||
</div>
|
||||
{sublabel && (
|
||||
<div class={`text-xs mt-1 ${light ? 'text-hds-line' : ''}`} style={light ? 'color: #C8C2B8' : 'color: var(--hds-fg-muted)'}>
|
||||
{sublabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import type { TranslationKey } from '@/i18n/ui';
|
||||
import StatCounter from './StatCounter.astro';
|
||||
|
||||
interface Props {
|
||||
t: (key: TranslationKey) => string;
|
||||
}
|
||||
const { t } = Astro.props;
|
||||
|
||||
const stats = [
|
||||
{ value: '26+', label: 'Años de experiencia', sub: 'Desde el 2000' },
|
||||
{ value: '100+', label: 'Proyectos hospedados', sub: 'Webs, e-commerce, infra' },
|
||||
{ value: '4', label: 'Países', sub: 'Canadá, USA, Alemania, UY' },
|
||||
{ value: 'Senior', label: 'Equipo senior', sub: 'SysAdmin / SRE' },
|
||||
];
|
||||
---
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg); border-top: 1px solid var(--hds-line); border-bottom: 1px solid var(--hds-line);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-10 md:gap-8">
|
||||
{stats.map((stat) => (
|
||||
<div class="text-center">
|
||||
<div class="font-display leading-none mb-3" style="font-size: clamp(3rem, 6vw, 4.5rem); color: var(--color-hds-naranja); font-weight: 500;">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div class="text-sm font-semibold" style="color: var(--hds-fg-soft);">
|
||||
{stat.label}
|
||||
</div>
|
||||
<div class="text-xs mt-1" style="color: var(--hds-fg-muted);">
|
||||
{stat.sub}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
import { DEFAULT_LANG, type Lang } from '@/i18n/utils';
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
}
|
||||
const { lang } = Astro.props;
|
||||
|
||||
const isDark = lang === 'en' ? 'dark' : 'light';
|
||||
---
|
||||
<button
|
||||
type="button"
|
||||
id="theme-toggle"
|
||||
aria-label={lang === 'en' ? 'Switch theme' : 'Cambiar tema'}
|
||||
class="p-2 rounded-md hover:bg-hds-line transition-colors"
|
||||
style="color: var(--hds-fg-soft);"
|
||||
>
|
||||
<svg class="theme-icon-sun w-5 h-5 hidden dark:block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4"/>
|
||||
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
|
||||
</svg>
|
||||
<svg class="theme-icon-moon w-5 h-5 block dark:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
const btn = document.getElementById('theme-toggle');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', () => {
|
||||
const isDark = document.documentElement.classList.toggle('dark');
|
||||
try { localStorage.setItem('theme', isDark ? 'dark' : 'light'); } catch(e) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
import type { Lang } from '@/i18n/utils';
|
||||
|
||||
interface Props {
|
||||
lang: Lang;
|
||||
}
|
||||
|
||||
const { lang } = Astro.props;
|
||||
const label = lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp';
|
||||
---
|
||||
<a
|
||||
href="https://wa.me/59899812487"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="fixed bottom-6 right-6 z-30 inline-flex items-center justify-center w-14 h-14 rounded-full text-white shadow-xl hover:scale-105 transition-transform"
|
||||
style="background: #25D366;"
|
||||
aria-label={label}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<path d="M14 30 Q8 24 14 18" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M34 30 Q40 24 34 18" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="20" y="16" width="8" height="20" rx="2" fill="#FA9F5C"/>
|
||||
<circle cx="24" cy="10" r="2" fill="#EE7623"/>
|
||||
<path d="M22 12 Q22 14 24 16 M26 12 Q26 14 24 16" stroke="#EE7623" stroke-width="1.2" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<path d="M16 8 L8 24 L16 40" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32 8 L40 24 L32 40" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28 10 L20 38" stroke="#FA9F5C" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M11 28 L37 28" stroke="#FA9F5C" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<rect x="6" y="6" width="3" height="3" fill="#FFCBA1"/>
|
||||
<rect x="39" y="6" width="3" height="3" fill="#FFCBA1"/>
|
||||
<rect x="6" y="39" width="3" height="3" fill="#FFCBA1"/>
|
||||
<rect x="39" y="39" width="3" height="3" fill="#FFCBA1"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<circle cx="24" cy="24" r="18" fill="none" stroke="#EE7623" stroke-width="2"/>
|
||||
<ellipse cx="24" cy="24" rx="8" ry="18" fill="none" stroke="#EE7623" stroke-width="1.5"/>
|
||||
<path d="M6 24 L42 24" stroke="#EE7623" stroke-width="1.5"/>
|
||||
<path d="M9 14 Q24 18 39 14" stroke="#FA9F5C" stroke-width="1.5" fill="none"/>
|
||||
<path d="M9 34 Q24 30 39 34" stroke="#FA9F5C" stroke-width="1.5" fill="none"/>
|
||||
<circle cx="24" cy="24" r="2" fill="#EE7623"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<rect x="8" y="14" width="26" height="20" rx="2" fill="#EE7623"/>
|
||||
<path d="M34 18 L38 18 Q42 18 42 22 Q42 26 38 26 L34 26" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M34 22 L38 22 Q40 22 40 24" stroke="#FA9F5C" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M21 8 L21 14" stroke="#FA9F5C" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M15 8 L15 14" stroke="#FA9F5C" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M27 8 L27 14" stroke="#FA9F5C" stroke-width="2" stroke-linecap="round"/>
|
||||
<rect x="12" y="20" width="6" height="2" fill="#FAF6EE" rx="1"/>
|
||||
<rect x="12" y="24" width="10" height="2" fill="#FAF6EE" rx="1" opacity="0.7"/>
|
||||
<rect x="12" y="28" width="8" height="2" fill="#FAF6EE" rx="1" opacity="0.5"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<circle cx="24" cy="32" r="6" fill="#EE7623"/>
|
||||
<path d="M24 26 L24 18" stroke="#EE7623" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M16 8 L16 18" stroke="#FA9F5C" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M32 8 L32 18" stroke="#FA9F5C" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<path d="M14 18 Q24 12 34 18" stroke="#EE7623" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M14 18 L34 18" stroke="#EE7623" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<rect x="11" y="6" width="4" height="4" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="17" y="6" width="4" height="4" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="27" y="6" width="4" height="4" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="33" y="6" width="4" height="4" fill="#FFCBA1" rx="0.5"/>
|
||||
<path d="M19 32 Q19 36 22 38" stroke="#FA9F5C" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
<path d="M29 32 Q29 36 26 38" stroke="#FA9F5C" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<rect x="8" y="12" width="14" height="10" rx="1" fill="#EE7623"/>
|
||||
<rect x="26" y="12" width="14" height="10" rx="1" fill="#EE7623"/>
|
||||
<rect x="8" y="26" width="14" height="10" rx="1" fill="#FA9F5C"/>
|
||||
<rect x="26" y="26" width="14" height="10" rx="1" fill="#FA9F5C"/>
|
||||
<circle cx="15" cy="17" r="1.5" fill="#FAF6EE"/>
|
||||
<circle cx="33" cy="17" r="1.5" fill="#FAF6EE"/>
|
||||
<circle cx="15" cy="31" r="1.5" fill="#FAF6EE"/>
|
||||
<circle cx="33" cy="31" r="1.5" fill="#FAF6EE"/>
|
||||
<path d="M22 17 L22 31" stroke="#FFCBA1" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M15 22 L33 22" stroke="#FFCBA1" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<path d="M10 14 L10 38 L38 38 L38 18 L30 10 L10 14 Z" fill="#EE7623" stroke="#EE7623" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M30 10 L30 18 L38 18" fill="none" stroke="#FAF6EE" stroke-width="2" stroke-linejoin="round"/>
|
||||
<rect x="14" y="22" width="20" height="2" fill="#FAF6EE" rx="1"/>
|
||||
<rect x="14" y="26" width="14" height="2" fill="#FAF6EE" rx="1" opacity="0.7"/>
|
||||
<rect x="14" y="30" width="10" height="2" fill="#FAF6EE" rx="1" opacity="0.5"/>
|
||||
<path d="M32 28 L40 22 L40 32 L32 36 Z" fill="#FA9F5C" stroke="#FA9F5C" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<circle cx="36" cy="29" r="1.2" fill="#FAF6EE"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<rect x="10" y="20" width="28" height="20" rx="2" fill="none" stroke="#EE7623" stroke-width="2.5"/>
|
||||
<path d="M18 20 L18 14 Q18 10 22 10 L26 10 Q30 10 30 14 L30 20" fill="none" stroke="#EE7623" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<circle cx="24" cy="30" r="2.5" fill="#FA9F5C"/>
|
||||
<path d="M24 32.5 L24 36" stroke="#FA9F5C" stroke-width="2" stroke-linecap="round"/>
|
||||
<rect x="8" y="38" width="32" height="2" fill="#FFCBA1" rx="1"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<rect x="8" y="14" width="22" height="20" rx="1" fill="#EE7623"/>
|
||||
<path d="M30 14 L40 14 L40 34 L30 34" fill="none" stroke="#EE7623" stroke-width="2.5"/>
|
||||
<path d="M30 14 L30 34" stroke="#EE7623" stroke-width="2.5"/>
|
||||
<path d="M34 18 L40 18 M34 22 L40 22 M34 26 L40 26" stroke="#FA9F5C" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<path d="M12 20 L20 20 M12 24 L26 24 M12 28 L22 28" stroke="#FAF6EE" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M22 30 L26 26 M26 30 L22 26" stroke="#FFCBA1" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<circle cx="32" cy="10" r="3" fill="#FA9F5C"/>
|
||||
<path d="M32 8 L32 12 M30 10 L34 10" stroke="#FAF6EE" stroke-width="1.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = 'w-12 h-12' } = Astro.props;
|
||||
---
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none" class={className} aria-hidden="true">
|
||||
<rect x="8" y="10" width="32" height="28" rx="3" fill="none" stroke="#EE7623" stroke-width="2.5"/>
|
||||
<circle cx="24" cy="24" r="8" fill="none" stroke="#EE7623" stroke-width="2.5"/>
|
||||
<path d="M16 14 L24 22 L32 14" stroke="#FA9F5C" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="11" y="6" width="3" height="3" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="14" y="6" width="3" height="3" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="31" y="6" width="3" height="3" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="34" y="6" width="3" height="3" fill="#FFCBA1" rx="0.5"/>
|
||||
<rect x="6" y="11" width="3" height="3" fill="#FA9F5C" rx="0.5"/>
|
||||
<rect x="6" y="14" width="3" height="3" fill="#FA9F5C" rx="0.5"/>
|
||||
<rect x="39" y="31" width="3" height="3" fill="#FA9F5C" rx="0.5"/>
|
||||
<rect x="39" y="34" width="3" height="3" fill="#FA9F5C" rx="0.5"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
const { class: className = '' } = Astro.props;
|
||||
---
|
||||
<svg viewBox="0 0 1000 500" xmlns="http://www.w3.org/2000/svg" class={className} aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="mapGrad" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#EE7623" stop-opacity="0.05"/>
|
||||
<stop offset="1" stop-color="#EE7623" stop-opacity="0.12"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<path
|
||||
d="M 130,140 Q 90,180 110,230 Q 90,290 140,330 Q 180,360 230,340 Q 280,330 290,290 Q 320,250 300,210 Q 280,160 230,140 Q 180,120 130,140 Z"
|
||||
fill="url(#mapGrad)" stroke="var(--color-hds-naranja)" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M 280,180 Q 320,150 380,160 Q 440,150 470,180 Q 480,210 460,240 Q 420,270 380,260 Q 330,250 300,230 Q 270,210 280,180 Z"
|
||||
fill="url(#mapGrad)" stroke="var(--color-hds-naranja)" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M 470,200 Q 510,180 560,190 Q 610,200 620,240 Q 600,280 550,290 Q 500,290 470,260 Q 450,230 470,200 Z"
|
||||
fill="url(#mapGrad)" stroke="var(--color-hds-naranja)" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5"
|
||||
/>
|
||||
<path
|
||||
d="M 620,290 Q 670,300 720,330 Q 770,360 800,400 Q 820,440 780,460 Q 730,470 680,440 Q 640,410 620,370 Q 600,330 620,290 Z"
|
||||
fill="url(#mapGrad)" stroke="var(--color-hds-naranja)" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5"
|
||||
/>
|
||||
|
||||
<g>
|
||||
<circle cx="170" cy="240" r="8" fill="var(--color-hds-naranja)">
|
||||
<animate attributeName="r" values="6;10;6" dur="2.5s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.4;0;0.4" dur="2.5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="170" cy="240" r="5" fill="var(--color-hds-naranja)"/>
|
||||
<text x="170" y="220" text-anchor="middle" font-size="14" font-weight="600" fill="var(--hds-fg)">Canadá</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<circle cx="370" cy="210" r="8" fill="var(--color-hds-naranja)">
|
||||
<animate attributeName="r" values="6;10;6" dur="2.5s" begin="0.4s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.4;0;0.4" dur="2.5s" begin="0.4s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="370" cy="210" r="5" fill="var(--color-hds-naranja)"/>
|
||||
<text x="370" y="190" text-anchor="middle" font-size="14" font-weight="600" fill="var(--hds-fg)">USA</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<circle cx="540" cy="230" r="8" fill="var(--color-hds-naranja)">
|
||||
<animate attributeName="r" values="6;10;6" dur="2.5s" begin="0.8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.4;0;0.4" dur="2.5s" begin="0.8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="540" cy="230" r="5" fill="var(--color-hds-naranja)"/>
|
||||
<text x="540" y="210" text-anchor="middle" font-size="14" font-weight="600" fill="var(--hds-fg)">Alemania</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<circle cx="720" cy="390" r="10" fill="var(--color-hds-naranja)">
|
||||
<animate attributeName="r" values="8;14;8" dur="2.5s" begin="1.2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.4;0;0.4" dur="2.5s" begin="1.2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="720" cy="390" r="6" fill="var(--color-hds-naranja)"/>
|
||||
<circle cx="720" cy="390" r="2.5" fill="white"/>
|
||||
<text x="720" y="370" text-anchor="middle" font-size="14" font-weight="600" fill="var(--hds-fg)">Uruguay</text>
|
||||
<text x="720" y="425" text-anchor="middle" font-size="10" fill="var(--hds-fg-muted)">Sede operativa</text>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -0,0 +1,103 @@
|
||||
# Contact Form — PHP endpoint
|
||||
|
||||
Este endpoint vive en `server1` y procesa los envíos del formulario de `/contacto`.
|
||||
|
||||
## ⚠️ NO se deploya con Astro. Es solo template.
|
||||
|
||||
## Setup en server1 (Netcup, cPanel)
|
||||
|
||||
### 1. Crear cuenta de correo dedicada
|
||||
|
||||
En cPanel → Email Accounts, crear:
|
||||
|
||||
- **Email**: `no-reply@hostingdelsur.net` (Mauri confirma que ya existe)
|
||||
- **Password**: contraseña fuerte (mínimo 16 caracteres, símbolos, números)
|
||||
- **Quota**: 1 GB (solo se usa para SMTP, no recibe correo)
|
||||
|
||||
### 2. Crear archivo de credenciales
|
||||
|
||||
El archivo se crea en `/home/hostingdelsur/.smtp-credentials.json` (NO en `public_html/`, por seguridad).
|
||||
|
||||
Contenido (reemplazar PEGAR_AQUI con la password real):
|
||||
|
||||
```json
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 465,
|
||||
"username": "no-reply@hostingdelsur.net",
|
||||
"password": "PEGAR_AQUI",
|
||||
"encryption": "ssl",
|
||||
"from": "no-reply@hostingdelsur.net",
|
||||
"fromName": "Hosting del Sur - Web",
|
||||
"to": "contacto@hostingdelsur.net"
|
||||
}
|
||||
```
|
||||
|
||||
Por SSH:
|
||||
|
||||
```bash
|
||||
chmod 600 /home/hostingdelsur/.smtp-credentials.json
|
||||
chown hostingdelsur:hostingdelsur /home/hostingdelsur/.smtp-credentials.json
|
||||
```
|
||||
|
||||
### 3. Subir PHPMailer (ya subido en este deploy)
|
||||
|
||||
```bash
|
||||
# Ya ejecutado durante el deploy:
|
||||
mkdir -p /home/hostingd/private/PHPMailer
|
||||
# 3 archivos copiados: Exception.php, PHPMailer.php, SMTP.php
|
||||
chmod 640 /home/hostingd/private/PHPMailer/*.php
|
||||
chown -R hostingdelsur:hostingdelsur /home/hostingd/private
|
||||
```
|
||||
|
||||
### 4. Subir el endpoint (ya subido)
|
||||
|
||||
```
|
||||
/home/hostingd/public_html/api/contact.php
|
||||
/home/hostingd/public_html/api/.htaccess
|
||||
```
|
||||
|
||||
El `.htaccess` deniega todo excepto POST al endpoint. Ya configurado.
|
||||
|
||||
### 5. Variables de entorno opcionales (reCAPTCHA secret)
|
||||
|
||||
Para activar validación reCAPTCHA v3 server-side, agregar en el `.htaccess` o php.ini del server:
|
||||
|
||||
```bash
|
||||
# SetEnv RECAPTCHA_SECRET "6LeOxaEaAAAAAM5OODBE2p9bknosuxpW7Gg17fKG"
|
||||
```
|
||||
|
||||
O en cPanel → MultiPHP INI Editor:
|
||||
|
||||
```ini
|
||||
env[RECAPTCHA_SECRET] = "6LeOxaEaAAAAAM5OODBE2p9bknosuxpW7Gg17fKG"
|
||||
```
|
||||
|
||||
### 6. Test
|
||||
|
||||
```bash
|
||||
curl -X POST https://hostingdelsur.net/api/contact.php \
|
||||
-d "name=Test&[email protected]&message=Hola&lang=es"
|
||||
```
|
||||
|
||||
Debe devolver `{"ok":true}`. Sin reCAPTCHA token primero, con token después.
|
||||
|
||||
## Seguridad implementada
|
||||
|
||||
- ✅ Credenciales fuera de `public_html/` (en `~/`)
|
||||
- ✅ Permisos `chmod 600` (solo el usuario puede leer)
|
||||
- ✅ Honeypot (`website` field) anti-bot
|
||||
- ✅ Validación y sanitización de inputs
|
||||
- ✅ `Reply-To` con email del visitante para responder
|
||||
- ✅ `From` con la cuenta dedicada (no spoofing)
|
||||
- ✅ TLS obligatorio (puerto 465 + `ssl`)
|
||||
- ✅ Rate limit básico por IP (3 envíos/hora) vía `apcu_fetch/store`
|
||||
- ✅ Headers anti-cache y anti-sniff
|
||||
- ✅ Mensaje de error genérico (no expone detalles internos)
|
||||
- ✅ reCAPTCHA v3 invisible (con score ≥ 0.3) si se configura `RECAPTCHA_SECRET`
|
||||
|
||||
## Pendiente
|
||||
|
||||
- [ ] Mauri debe re-enviar la password de `no-reply@` (Mauri la puso en chat originalmente; el deploy la borró del servidor)
|
||||
- [ ] Mauri debe crear el archivo `~/.smtp-credentials.json` con la password real
|
||||
- [ ] (Opcional) Configurar `RECAPTCHA_SECRET` en el environment del server
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
/**
|
||||
* hostingdelsur.net — Contact form endpoint
|
||||
*
|
||||
* Lee credenciales SMTP de ~/.smtp-credentials.json (fuera de public_html)
|
||||
* y envía el mail vía PHPMailer. Valida reCAPTCHA v3 (opcional si no hay token).
|
||||
*
|
||||
* Setup: ver src/forms/README.md
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Referrer-Policy: same-origin');
|
||||
header('X-Frame-Options: DENY');
|
||||
|
||||
require_once __DIR__ . '/../../private/PHPMailer/Exception.php';
|
||||
require_once __DIR__ . '/../../private/PHPMailer/PHPMailer.php';
|
||||
require_once __DIR__ . '/../../private/PHPMailer/SMTP.php';
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
header('Allow: POST');
|
||||
echo json_encode(['ok' => false, 'error' => 'Método no permitido']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$reCAPTCHA_secret = getenv('RECAPTCHA_SECRET') ?: ($_SERVER['RECAPTCHA_SECRET'] ?? '');
|
||||
|
||||
$credsPath = (($_SERVER['HOME'] ?? '') ?: '/home/hostingdelsur') . '/.smtp-credentials.json';
|
||||
if (!is_readable($credsPath)) {
|
||||
error_log('contact.php: credenciales no disponibles en ' . $credsPath);
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'Configuración no disponible']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$credsRaw = file_get_contents($credsPath);
|
||||
if ($credsRaw === false) {
|
||||
throw new RuntimeException('No se pudo leer el archivo de credenciales');
|
||||
}
|
||||
$creds = json_decode($credsRaw, true, 8, JSON_THROW_ON_ERROR);
|
||||
} catch (Throwable $e) {
|
||||
error_log('contact.php: credenciales inválidas — ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'Configuración inválida']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($_POST['website'])) {
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$name = trim((string) filter_input(INPUT_POST, 'name', FILTER_SANITIZE_SPECIAL_CHARS));
|
||||
$email = trim((string) filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL));
|
||||
$message = trim((string) filter_input(INPUT_POST, 'message', FILTER_SANITIZE_SPECIAL_CHARS));
|
||||
$lang = in_array($_POST['lang'] ?? '', ['es', 'en'], true) ? $_POST['lang'] : 'es';
|
||||
$recaptchaToken = trim((string) ($_POST['g-recaptcha-response'] ?? ''));
|
||||
|
||||
if ($name === '' || !$email || $message === '' || mb_strlen($message) > 5000) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Datos inválidos']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($reCAPTCHA_secret !== '' && $recaptchaToken !== '') {
|
||||
$verifyUrl = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $verifyUrl,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'secret' => $reCAPTCHA_secret,
|
||||
'response' => $recaptchaToken,
|
||||
'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
|
||||
]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response !== false && $httpCode === 200) {
|
||||
$verification = json_decode($response, true);
|
||||
if (!is_array($verification) || empty($verification['success']) || ($verification['score'] ?? 1) < 0.3) {
|
||||
error_log('contact.php: reCAPTCHA failed — ' . json_encode($verification));
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Verificación de seguridad falló']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} elseif ($reCAPTCHA_secret !== '' && $recaptchaToken === '') {
|
||||
error_log('contact.php: reCAPTCHA token missing');
|
||||
}
|
||||
|
||||
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
|
||||
$rateKey = "contact_rl_{$ip}";
|
||||
if (function_exists('apcu_fetch')) {
|
||||
$hits = apcu_fetch($rateKey) ?: 0;
|
||||
if ($hits >= 3) {
|
||||
http_response_code(429);
|
||||
header('Retry-After: 3600');
|
||||
echo json_encode(['ok' => false, 'error' => 'Demasiados envíos. Probá más tarde.']);
|
||||
exit;
|
||||
}
|
||||
apcu_store($rateKey, $hits + 1, 3600);
|
||||
}
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
$mail->isSMTP();
|
||||
$mail->Host = (string) $creds['host'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = (string) $creds['username'];
|
||||
$mail->Password = (string) $creds['password'];
|
||||
$mail->SMTPSecure = (string) ($creds['encryption'] ?? 'ssl');
|
||||
$mail->Port = (int) ($creds['port'] ?? 465);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
$mail->Timeout = 10;
|
||||
|
||||
$mail->setFrom((string) $creds['from'], (string) ($creds['fromName'] ?? 'Hosting del Sur'));
|
||||
$mail->addAddress((string) $creds['to']);
|
||||
$mail->addReplyTo($email, $name);
|
||||
|
||||
$mail->Subject = "Contacto desde hostingdelsur.net: {$name}";
|
||||
$body = "Nombre: {$name}\nEmail: {$email}\nIdioma: {$lang}\nIP: {$ip}\n\nMensaje:\n{$message}";
|
||||
$mail->Body = $body;
|
||||
$mail->AltBody = $body;
|
||||
|
||||
$mail->send();
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (Exception $e) {
|
||||
error_log('contact.php: PHPMailer error — ' . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'No se pudo enviar el mensaje']);
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
export const ui = {
|
||||
es: {
|
||||
'nav.home': 'Inicio',
|
||||
'nav.services': 'Servicios',
|
||||
'nav.plans': 'Planes',
|
||||
'nav.about': 'Nosotros',
|
||||
'nav.tutorials': 'Instructivos',
|
||||
'nav.contact': 'Contacto',
|
||||
'nav.cta': 'Hablar por WhatsApp',
|
||||
'hero.eyebrow': 'Tu proveedor de soluciones digitales',
|
||||
'hero.title': 'Alojamiento en la nube, cerca y en tu idioma',
|
||||
'hero.body': 'Olvidate de las complejidades técnicas. Nuestro equipo te brinda asesoramiento y soporte personalizado para que tu proyecto en la web funcione sin complicaciones. Te brindamos la misma tecnología que los gigantes tecnológicos, con una atención cercana y personalizada.',
|
||||
'hero.ctaPrimary': 'Hablar con un humano',
|
||||
'hero.ctaSecondary': 'Ver servicios',
|
||||
'diff.eyebrow': 'La diferencia',
|
||||
'diff.title': 'Por qué elegirnos',
|
||||
'diff.subtitle': 'Tu proyecto merece un socio tecnológico, no solo un hosting.',
|
||||
'diff.support.title': 'Soporte técnico real',
|
||||
'diff.support.body': 'Olvidate de los intermediarios y los tickets infinitos. Hablás directamente con profesionales senior (SysAdmin/SRE) que entienden tu negocio y resuelven de verdad.',
|
||||
'diff.sovereignty.title': 'Soberanía y privacidad de datos',
|
||||
'diff.sovereignty.body': 'Ponemos el foco en que seas el único dueño de tu información. Sabemos exactamente dónde están tus datos y quién tiene acceso a ellos, garantizando una privacidad que las nubes masivas no ofrecen.',
|
||||
'diff.infra.title': 'Infraestructura global y escalable',
|
||||
'diff.infra.body': 'Con presencia internacional desde el año 2000 y tecnología Open Source (GNU/Linux), te brindamos una plataforma robusta que te permite crecer sin pagar costos excesivos de licenciamiento.',
|
||||
'services.eyebrow': 'Lo que hacemos',
|
||||
'services.title': 'Nuestros servicios',
|
||||
'services.subtitle': 'Acompañamos a tu empresa en cada paso, desde el hosting hasta la infraestructura compleja.',
|
||||
'services.cat.hosting': 'Hosting & Correo',
|
||||
'services.cat.design': 'Diseño & Contenido',
|
||||
'services.cat.infra': 'Infraestructura',
|
||||
'services.viewDetail': 'Ver detalle',
|
||||
'process.eyebrow': 'Cómo trabajamos',
|
||||
'process.title': 'Del primer mate al proyecto en marcha',
|
||||
'process.step1.title': 'Charlamos',
|
||||
'process.step1.body': 'Nos contás tu proyecto. Te escuchamos sin tecnicismos ni promesas vacías.',
|
||||
'process.step2.title': 'Diseñamos',
|
||||
'process.step2.body': 'Te proponemos la mejor solución a tu medida, con costos claros.',
|
||||
'process.step3.title': 'Desplegamos',
|
||||
'process.step3.body': 'Implementamos todo sin que tengas que preocuparte por detalles técnicos.',
|
||||
'process.step4.title': 'Te acompañamos',
|
||||
'process.step4.body': 'Soporte humano continuo, no tickets automáticos ni respuestas genéricas.',
|
||||
'ctaFinal.eyebrow': 'Hablemos',
|
||||
'ctaFinal.title': '¿Listo para charlar?',
|
||||
'ctaFinal.body': 'Escribinos por WhatsApp y te respondemos personas reales, no bots.',
|
||||
'ctaFinal.button': 'Hablar por WhatsApp',
|
||||
'footer.tagline': 'Alojamiento en la nube, cerca y en tu idioma.',
|
||||
'footer.contact': 'Contacto',
|
||||
'footer.nav': 'Navegación',
|
||||
'footer.legal': 'Legal',
|
||||
'footer.privacy': 'Privacidad',
|
||||
'footer.terms': 'Términos',
|
||||
'footer.copyright': 'Todos los derechos reservados.',
|
||||
'lang.toggle': 'EN',
|
||||
'lang.label': 'Cambiar idioma',
|
||||
},
|
||||
en: {
|
||||
'nav.home': 'Home',
|
||||
'nav.services': 'Services',
|
||||
'nav.plans': 'Plans',
|
||||
'nav.about': 'About',
|
||||
'nav.tutorials': 'Tutorials',
|
||||
'nav.contact': 'Contact',
|
||||
'nav.cta': 'Chat on WhatsApp',
|
||||
'hero.eyebrow': 'Your digital solutions provider',
|
||||
'hero.title': 'Cloud hosting, close by and in your language',
|
||||
'hero.body': 'Forget technical complexity. Our team gives you personalized advice and support so your web project runs smoothly. We offer the same technology as the tech giants, with a close, human touch.',
|
||||
'hero.ctaPrimary': 'Talk to a human',
|
||||
'hero.ctaSecondary': 'See services',
|
||||
'diff.eyebrow': 'The difference',
|
||||
'diff.title': 'Why choose us',
|
||||
'diff.subtitle': 'Your project deserves a technology partner, not just a hosting provider.',
|
||||
'diff.support.title': 'Real technical support',
|
||||
'diff.support.body': 'Forget middlemen and endless tickets. You talk directly to senior professionals (SysAdmin/SRE) who understand your business and actually solve things.',
|
||||
'diff.sovereignty.title': 'Data sovereignty and privacy',
|
||||
'diff.sovereignty.body': 'We focus on you being the sole owner of your data. We know exactly where your data is and who has access, guaranteeing a privacy that massive clouds cannot offer.',
|
||||
'diff.infra.title': 'Global, scalable infrastructure',
|
||||
'diff.infra.body': 'With international presence since 2000 and Open Source technology (GNU/Linux), we offer a robust platform that lets you grow without paying excessive licensing fees.',
|
||||
'services.eyebrow': 'What we do',
|
||||
'services.title': 'Our services',
|
||||
'services.subtitle': 'We support your business at every step, from hosting to complex infrastructure.',
|
||||
'services.cat.hosting': 'Hosting & Email',
|
||||
'services.cat.design': 'Design & Content',
|
||||
'services.cat.infra': 'Infrastructure',
|
||||
'services.viewDetail': 'View details',
|
||||
'process.eyebrow': 'How we work',
|
||||
'process.title': 'From the first chat to a live project',
|
||||
'process.step1.title': 'We listen',
|
||||
'process.step1.body': 'Tell us about your project. We listen without jargon or empty promises.',
|
||||
'process.step2.title': 'We design',
|
||||
'process.step2.body': 'We propose the best tailor-made solution, with clear costs.',
|
||||
'process.step3.title': 'We deploy',
|
||||
'process.step3.body': 'We handle all the implementation so you do not worry about technical details.',
|
||||
'process.step4.title': 'We support',
|
||||
'process.step4.body': 'Continuous human support, not automated tickets or generic answers.',
|
||||
'ctaFinal.eyebrow': 'Let us talk',
|
||||
'ctaFinal.title': 'Ready to chat?',
|
||||
'ctaFinal.body': 'Message us on WhatsApp and real people will reply, not bots.',
|
||||
'ctaFinal.button': 'Chat on WhatsApp',
|
||||
'footer.tagline': 'Cloud hosting, close by and in your language.',
|
||||
'footer.contact': 'Contact',
|
||||
'footer.nav': 'Navigation',
|
||||
'footer.legal': 'Legal',
|
||||
'footer.privacy': 'Privacy',
|
||||
'footer.terms': 'Terms',
|
||||
'footer.copyright': 'All rights reserved.',
|
||||
'lang.toggle': 'ES',
|
||||
'lang.label': 'Switch language',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Lang = keyof typeof ui;
|
||||
export type TranslationKey = keyof typeof ui.es;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ui, type Lang, type TranslationKey } from './ui';
|
||||
|
||||
export const LANGUAGES: { code: Lang; label: string }[] = [
|
||||
{ code: 'es', label: 'ES' },
|
||||
{ code: 'en', label: 'EN' },
|
||||
];
|
||||
|
||||
export const DEFAULT_LANG: Lang = 'es';
|
||||
|
||||
export function getLangFromUrl(url: URL): Lang {
|
||||
const [, lang] = url.pathname.split('/');
|
||||
return lang === 'en' ? 'en' : 'es';
|
||||
}
|
||||
|
||||
export function useTranslations(lang: Lang) {
|
||||
return function t(key: TranslationKey): string {
|
||||
return ui[lang][key] ?? ui.es[key];
|
||||
};
|
||||
}
|
||||
|
||||
export function pathForLang(path: string, targetLang: Lang): string {
|
||||
const cleanPath = path.replace(/^\/(en|es)/, '') || '/';
|
||||
if (targetLang === DEFAULT_LANG) return cleanPath;
|
||||
return `/${targetLang}${cleanPath === '/' ? '' : cleanPath}`;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
import { getLangFromUrl, useTranslations, type Lang } from '@/i18n/utils';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
ogImage?: string;
|
||||
canonical?: string;
|
||||
lang?: Lang;
|
||||
pageType?: 'website' | 'article';
|
||||
}
|
||||
|
||||
const { title, description, ogImage = '/og-image.jpg', canonical, lang, pageType = 'website' } = Astro.props;
|
||||
const currentLang = lang ?? getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(currentLang);
|
||||
const siteUrl = Astro.site?.toString().replace(/\/$/, '') ?? 'https://hostingdelsur.net';
|
||||
const canonicalUrl = canonical ? `${siteUrl}${canonical}` : `${siteUrl}${Astro.url.pathname}`;
|
||||
|
||||
import '@/styles/global.css';
|
||||
import Navbar from '@/components/Navbar.astro';
|
||||
import Footer from '@/components/Footer.astro';
|
||||
import WhatsappFab from '@/components/WhatsappFab.astro';
|
||||
import BackToTop from '@/components/BackToTop.astro';
|
||||
|
||||
const orgJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Organization',
|
||||
name: 'Hosting del Sur',
|
||||
url: siteUrl,
|
||||
logo: `${siteUrl}/logo/hds-color.svg`,
|
||||
description: description,
|
||||
telephone: '+59899812487',
|
||||
email: 'contacto@hostingdelsur.net',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressCountry: 'UY',
|
||||
addressLocality: 'Maldonado',
|
||||
},
|
||||
areaServed: [
|
||||
{ '@type': 'Country', name: 'Uruguay' },
|
||||
{ '@type': 'Country', name: 'Canada' },
|
||||
{ '@type': 'Country', name: 'United States' },
|
||||
{ '@type': 'Country', name: 'Germany' },
|
||||
],
|
||||
};
|
||||
|
||||
const localBusinessJsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfessionalService',
|
||||
name: 'Hosting del Sur',
|
||||
image: `${siteUrl}/og-image.jpg`,
|
||||
'@id': siteUrl,
|
||||
url: siteUrl,
|
||||
telephone: '+59899812487',
|
||||
email: 'contacto@hostingdelsur.net',
|
||||
priceRange: '$$',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressCountry: 'UY',
|
||||
addressLocality: 'Maldonado',
|
||||
},
|
||||
areaServed: [
|
||||
{ '@type': 'Country', name: 'Uruguay' },
|
||||
{ '@type': 'Country', name: 'Canada' },
|
||||
{ '@type': 'Country', name: 'United States' },
|
||||
{ '@type': 'Country', name: 'Germany' },
|
||||
],
|
||||
};
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang={currentLang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#FAF6EE" media="(prefers-color-scheme: light)" />
|
||||
<meta name="theme-color" content="#1A1612" media="(prefers-color-scheme: dark)" />
|
||||
<title>{title} | Hosting del Sur</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<meta property="og:type" content={pageType} />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:title" content={`${title} | Hosting del Sur`} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`${siteUrl}${ogImage}`} />
|
||||
<meta property="og:locale" content={currentLang === 'en' ? 'en_US' : 'es_UY'} />
|
||||
<meta property="og:site_name" content="Hosting del Sur" />
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={`${title} | Hosting del Sur`} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${siteUrl}${ogImage}`} />
|
||||
|
||||
<link rel="alternate" hreflang="es" href={`${siteUrl}${currentLang === 'en' ? '/' : Astro.url.pathname.replace(/^\/en/, '')}`} />
|
||||
<link rel="alternate" hreflang="en" href={`${siteUrl}/en${Astro.url.pathname.replace(/^\/en/, '') || ''}`} />
|
||||
|
||||
<script type="application/ld+json" set:html={JSON.stringify(orgJsonLd)} />
|
||||
<script type="application/ld+json" set:html={JSON.stringify(localBusinessJsonLd)} />
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
try {
|
||||
var stored = localStorage.getItem('theme');
|
||||
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
var theme = stored || (prefersDark ? 'dark' : 'light');
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
} catch(e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main" class="skip-link">
|
||||
{currentLang === 'en' ? 'Skip to content' : 'Saltar al contenido'}
|
||||
</a>
|
||||
<Navbar t={t} lang={currentLang} />
|
||||
<main id="main">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer t={t} lang={currentLang} />
|
||||
<WhatsappFab lang={currentLang} />
|
||||
<BackToTop />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
export interface Plan {
|
||||
slug: string;
|
||||
name: string;
|
||||
priceUsdYear: number;
|
||||
recommended?: boolean;
|
||||
features: string[];
|
||||
featuresEn?: string[];
|
||||
}
|
||||
|
||||
export const PLANS: Plan[] = [
|
||||
{
|
||||
slug: 'basico',
|
||||
name: 'Básico',
|
||||
priceUsdYear: 59,
|
||||
recommended: false,
|
||||
features: [
|
||||
'5 GB espacio en disco',
|
||||
'10 cuentas de correo',
|
||||
'SSL gratis (Let\'s Encrypt)',
|
||||
'Backups automáticos diarios',
|
||||
'cPanel incluido',
|
||||
'Soporte técnico real',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'institucional',
|
||||
name: 'Institucional',
|
||||
priceUsdYear: 129,
|
||||
recommended: true,
|
||||
features: [
|
||||
'15 GB espacio en disco',
|
||||
'30 cuentas de correo',
|
||||
'1 base de datos MySQL',
|
||||
'1 dominio parqueado',
|
||||
'250% CPU speed',
|
||||
'2 GB RAM',
|
||||
'Soporte prioritario',
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'e-commerce',
|
||||
name: 'E-commerce',
|
||||
priceUsdYear: 219,
|
||||
recommended: false,
|
||||
features: [
|
||||
'30 GB espacio en disco',
|
||||
'50 cuentas de correo',
|
||||
'3 bases de datos MySQL',
|
||||
'3 dominios parkados',
|
||||
'300% CPU speed',
|
||||
'2.5 GB RAM',
|
||||
'Soporte prioritario',
|
||||
'Optimización para WooCommerce',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const ADDITIONAL_PLANS = [
|
||||
'WordPress',
|
||||
'Empresarial',
|
||||
'Avanzado',
|
||||
'Avanzado HD',
|
||||
'Corporativo',
|
||||
'Corporativo HD',
|
||||
];
|
||||
|
||||
export function monthlyEquivalent(annual: number): string {
|
||||
return (annual / 12).toFixed(2);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
---
|
||||
<BaseLayout
|
||||
title="404"
|
||||
description="Página no encontrada"
|
||||
>
|
||||
<section class="py-32 bg-hds-marfil">
|
||||
<div class="max-w-2xl mx-auto px-4 text-center">
|
||||
<p class="text-6xl font-display text-hds-naranja mb-4">404</p>
|
||||
<h1 class="text-3xl md:text-4xl text-hds-tinta mb-4">Página no encontrada</h1>
|
||||
<p class="text-hds-grafito mb-8">
|
||||
La página que buscás no existe o fue movida.
|
||||
</p>
|
||||
<a
|
||||
href="/"
|
||||
class="inline-flex items-center gap-2 bg-hds-naranja hover:bg-hds-naranja-hover text-white text-sm font-semibold px-5 py-2.5 rounded-md transition-colors"
|
||||
>
|
||||
Volver al inicio
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
const reCAPTCHA_site_key = '6LeOxaEaAAAAAPD8rQQLqw4woxn1dMjvF_Ooy0BG';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Contact' : 'Contacto'}
|
||||
description={lang === 'en'
|
||||
? 'Talk to a real person at Hosting del Sur. WhatsApp, email or form.'
|
||||
: 'Hablá con una persona real en Hosting del Sur. WhatsApp, mail o formulario.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Contact' : 'Contacto'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Talk to a <span style="color: var(--color-hds-naranja); font-style: italic;">real person</span>.</>
|
||||
: <>Hablá con una <span style="color: var(--color-hds-naranja); font-style: italic;">persona real</span>.</>}
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'No bots, no tickets. Write to us on WhatsApp, email us, or fill out the form below.'
|
||||
: 'Sin bots, sin tickets. Escribinos por WhatsApp, mandanos un mail o completá el formulario.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<aside class="lg:col-span-1 space-y-4">
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">WhatsApp</h2>
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="text-lg font-semibold hover:underline break-all" style="color: var(--color-hds-naranja);">+598 99 812 487</a>
|
||||
<p class="text-sm mt-2" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en' ? 'Monday to Friday, 9 to 18 hs UYT' : 'Lunes a viernes, 9 a 18 hs UYT'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">Email</h2>
|
||||
<a href="mailto:contacto@hostingdelsur.net" class="text-lg font-semibold hover:underline break-all" style="color: var(--color-hds-naranja);">contacto@hostingdelsur.net</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">{lang === 'en' ? 'Location' : 'Ubicación'}</h2>
|
||||
<p style="color: var(--hds-fg-soft);">Maldonado, Uruguay</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<form
|
||||
action="/api/contact.php"
|
||||
method="POST"
|
||||
class="rounded-2xl p-8 md:p-10 space-y-5"
|
||||
style="background: var(--hds-card); border: 1px solid var(--hds-line);"
|
||||
id="contact-form"
|
||||
novalidate
|
||||
>
|
||||
<input type="hidden" name="lang" value={lang} />
|
||||
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" />
|
||||
<div class="absolute -left-[9999px]" aria-hidden="true">
|
||||
<label for="website">Website</label>
|
||||
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Name' : 'Nombre'} <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
autocomplete="name"
|
||||
maxlength="100"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
Email <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
maxlength="200"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Message' : 'Mensaje'} <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows="5"
|
||||
maxlength="5000"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors resize-y"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-2">
|
||||
<p class="text-xs" style="color: var(--hds-fg-muted);">
|
||||
{lang === 'en' ? 'We typically reply within 24 business hours.' : 'Respondemos típicamente dentro de las 24 hs hábiles.'}
|
||||
</p>
|
||||
<button type="submit" class="btn-primary text-sm">
|
||||
<span id="contact-submit-label">{lang === 'en' ? 'Send message' : 'Enviar mensaje'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="contact-status" role="status" aria-live="polite" class="hidden text-sm"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src={`https://www.google.com/recaptcha/api.js?render=${reCAPTCHA_site_key}`} async defer></script>
|
||||
<noscript>
|
||||
<p class="text-xs mt-2" style="color: var(--color-hds-error);">JavaScript is required for the form to work.</p>
|
||||
</noscript>
|
||||
<script is:inline define:vars={{ siteKey: reCAPTCHA_site_key }}>
|
||||
(function() {
|
||||
const form = document.getElementById('contact-form');
|
||||
if (!form) return;
|
||||
const status = document.getElementById('contact-status');
|
||||
const submit = form.querySelector('button[type="submit"]');
|
||||
const label = document.getElementById('contact-submit-label');
|
||||
const langInput = form.querySelector('input[name="lang"]');
|
||||
const isEn = langInput && langInput.value === 'en';
|
||||
const sendingLabel = isEn ? 'Sending…' : 'Enviando…';
|
||||
const successMsg = isEn ? 'Message sent. We will reply soon.' : 'Mensaje enviado. Te respondemos pronto.';
|
||||
const errorMsg = isEn ? 'Could not send. Please try again or contact us via WhatsApp.' : 'No se pudo enviar. Probá de nuevo o contactanos por WhatsApp.';
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (window.grecaptcha && window.grecaptcha.ready) {
|
||||
window.grecaptcha.ready();
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const originalLabel = label.textContent;
|
||||
label.textContent = sendingLabel;
|
||||
submit.disabled = true;
|
||||
status.classList.remove('hidden');
|
||||
status.className = 'text-sm';
|
||||
status.textContent = '';
|
||||
status.style.color = 'var(--hds-fg-soft)';
|
||||
|
||||
try {
|
||||
let token = '';
|
||||
if (window.grecaptcha && window.grecaptcha.execute) {
|
||||
try {
|
||||
token = await Promise.race([
|
||||
window.grecaptcha.execute(siteKey, { action: 'contact' }),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('recaptcha timeout')), 5000)),
|
||||
]);
|
||||
} catch (e) { /* fall through without token */ }
|
||||
}
|
||||
document.getElementById('g-recaptcha-response').value = token || '';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'fetch' },
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.ok) {
|
||||
status.style.color = 'var(--color-hds-exito)';
|
||||
status.textContent = successMsg;
|
||||
form.reset();
|
||||
} else {
|
||||
throw new Error(data.error || 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
status.style.color = 'var(--color-hds-error)';
|
||||
status.textContent = errorMsg;
|
||||
} finally {
|
||||
label.textContent = originalLabel;
|
||||
submit.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import WorldMap from '@/components/icons/WorldMap.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'About us' : 'Nosotros'}
|
||||
description={lang === 'en'
|
||||
? 'Since 2000, Hosting del Sur provides technology services with a human touch from Maldonado, Uruguay, across four countries.'
|
||||
: 'Desde 2000, Hosting del Sur brinda servicios de tecnología con atención humana desde Maldonado, Uruguay, con alcance en cuatro países.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'About us' : 'Nosotros'} />
|
||||
<h1 class="font-display leading-tight mt-5 max-w-4xl" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Technology with a <span style="color: var(--color-hds-naranja); font-style: italic;">human face</span> since 2000.</>
|
||||
: <>Tecnología con <span style="color: var(--color-hds-naranja); font-style: italic;">cara de persona</span> desde 2000.</>}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-16 md:py-20" style="background: var(--hds-bg);">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="prose prose-lg max-w-none" style="color: var(--hds-fg-soft);">
|
||||
<p class="text-lg leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We are a small team of senior SysAdmins and SREs based in Maldonado, Uruguay. We work with clients across Latin America and the world.'
|
||||
: 'Somos un equipo chico de SysAdmins y SREs senior, radicados en Maldonado, Uruguay. Trabajamos con clientes de toda Latinoamérica y el mundo.'}
|
||||
</p>
|
||||
|
||||
<h2 class="font-display mt-12 mb-4" style="color: var(--hds-fg); font-size: 2rem;">
|
||||
{lang === 'en' ? 'Our story' : 'Nuestra historia'}
|
||||
</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur was born in 2000 as a hosting services provider for Uruguayan businesses. From the beginning we were clear: we wanted to offer enterprise-level technology with the closeness of a small team. Twenty-six years later we are still the same — fewer tickets, real names, infrastructure we know by heart.'
|
||||
: 'Hosting del Sur nació en el año 2000 como proveedor de servicios de hosting para empresas uruguayas. Desde el principio teníamos claro: queríamos ofrecer tecnología de nivel enterprise con la cercanía de un equipo chico. Veintiséis años después seguimos siendo lo mismo — menos tickets, nombres reales, infraestructura que conocemos de memoria.'}
|
||||
</p>
|
||||
|
||||
<h2 class="font-display mt-12 mb-4" style="color: var(--hds-fg); font-size: 2rem;">
|
||||
{lang === 'en' ? 'What we believe' : 'En qué creemos'}
|
||||
</h2>
|
||||
<ul class="space-y-2" style="color: var(--hds-fg-soft);">
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'A real person answers.' : 'Una persona real te responde.'}</strong> {lang === 'en' ? 'Always.' : 'Siempre.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'You own your data.' : 'Tus datos son tuyos.'}</strong> {lang === 'en' ? 'We just host it.' : 'Nosotros solo los hospedamos.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Open source first.' : 'Open source primero.'}</strong> {lang === 'en' ? 'Vendor lock-in never.' : 'Vendor lock-in nunca.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'No magic, no jargon.' : 'Sin magia, sin jerga.'}</strong> {lang === 'en' ? 'We explain what we do.' : 'Explicamos lo que hacemos.'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<SectionEyebrow text={lang === 'en' ? 'Where we work' : 'Dónde trabajamos'} />
|
||||
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 4.5vw, 3rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Four countries,<br/><span style="color: var(--color-hds-naranja); font-style: italic;">one team</span>.</>
|
||||
: <>Cuatro países,<br/><span style="color: var(--color-hds-naranja); font-style: italic;">un equipo</span>.</>}
|
||||
</h2>
|
||||
<p class="text-lg mt-5" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We operate from Maldonado, Uruguay, with infrastructure distributed across four countries: Canada, the United States, Germany and Uruguay. We combine providers in three regions with our own equipment in Uruguay to give you redundancy, low latency and data sovereignty.'
|
||||
: 'Operamos desde Maldonado, Uruguay, con infraestructura distribuida en cuatro países: Canadá, Estados Unidos, Alemania y Uruguay. Combinamos proveedores en tres regiones con equipos propios en Uruguay para darte redundancia, baja latencia y soberanía de datos.'}
|
||||
</p>
|
||||
|
||||
<ul class="mt-8 space-y-3 text-sm" style="color: var(--hds-fg-soft);">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1.5 w-2 h-2 rounded-full flex-shrink-0" style="background: var(--color-hds-naranja);"></span>
|
||||
<span><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Canada, USA, Germany:' : 'Canadá, USA, Alemania:'}</strong> {lang === 'en' ? 'provider infrastructure.' : 'infraestructura de proveedores.'}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1.5 w-2 h-2 rounded-full flex-shrink-0" style="background: var(--color-hds-naranja);"></span>
|
||||
<span><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Uruguay:' : 'Uruguay:'}</strong> {lang === 'en' ? 'our own equipment and headquarters.' : 'equipos propios y sede operativa.'}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6 lg:p-8" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<WorldMap class="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,205 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
const reCAPTCHA_site_key = '6LeOxaEaAAAAAPD8rQQLqw4woxn1dMjvF_Ooy0BG';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Contact' : 'Contacto'}
|
||||
description={lang === 'en'
|
||||
? 'Talk to a real person at Hosting del Sur. WhatsApp, email or form.'
|
||||
: 'Hablá con una persona real en Hosting del Sur. WhatsApp, mail o formulario.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Contact' : 'Contacto'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Talk to a <span style="color: var(--color-hds-naranja); font-style: italic;">real person</span>.</>
|
||||
: <>Hablá con una <span style="color: var(--color-hds-naranja); font-style: italic;">persona real</span>.</>}
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'No bots, no tickets. Write to us on WhatsApp, email us, or fill out the form below.'
|
||||
: 'Sin bots, sin tickets. Escribinos por WhatsApp, mandanos un mail o completá el formulario.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<aside class="lg:col-span-1 space-y-4">
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">WhatsApp</h2>
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="text-lg font-semibold hover:underline break-all" style="color: var(--color-hds-naranja);">+598 99 812 487</a>
|
||||
<p class="text-sm mt-2" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en' ? 'Monday to Friday, 9 to 18 hs UYT' : 'Lunes a viernes, 9 a 18 hs UYT'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">Email</h2>
|
||||
<a href="mailto:contacto@hostingdelsur.net" class="text-lg font-semibold hover:underline break-all" style="color: var(--color-hds-naranja);">contacto@hostingdelsur.net</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<h2 class="text-xs font-semibold tracking-[0.25em] uppercase mb-3" style="color: var(--color-hds-naranja);">{lang === 'en' ? 'Location' : 'Ubicación'}</h2>
|
||||
<p style="color: var(--hds-fg-soft);">Maldonado, Uruguay</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="lg:col-span-2">
|
||||
<form
|
||||
action="/api/contact.php"
|
||||
method="POST"
|
||||
class="rounded-2xl p-8 md:p-10 space-y-5"
|
||||
style="background: var(--hds-card); border: 1px solid var(--hds-line);"
|
||||
id="contact-form"
|
||||
novalidate
|
||||
>
|
||||
<input type="hidden" name="lang" value={lang} />
|
||||
<input type="hidden" name="g-recaptcha-response" id="g-recaptcha-response" />
|
||||
<div class="absolute -left-[9999px]" aria-hidden="true">
|
||||
<label for="website">Website</label>
|
||||
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Name' : 'Nombre'} <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
autocomplete="name"
|
||||
maxlength="100"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
Email <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
maxlength="200"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium mb-1.5" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Message' : 'Mensaje'} <span style="color: var(--color-hds-error);">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows="5"
|
||||
maxlength="5000"
|
||||
class="w-full px-3.5 py-2.5 rounded-md outline-none transition-colors resize-y"
|
||||
style="background: var(--hds-bg); color: var(--hds-fg); border: 1px solid var(--hds-line);"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 pt-2">
|
||||
<p class="text-xs" style="color: var(--hds-fg-muted);">
|
||||
{lang === 'en' ? 'We typically reply within 24 business hours.' : 'Respondemos típicamente dentro de las 24 hs hábiles.'}
|
||||
</p>
|
||||
<button type="submit" class="btn-primary text-sm">
|
||||
<span id="contact-submit-label">{lang === 'en' ? 'Send message' : 'Enviar mensaje'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="contact-status" role="status" aria-live="polite" class="hidden text-sm"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src={`https://www.google.com/recaptcha/api.js?render=${reCAPTCHA_site_key}`} async defer></script>
|
||||
<noscript>
|
||||
<p class="text-xs mt-2" style="color: var(--color-hds-error);">JavaScript is required for the form to work.</p>
|
||||
</noscript>
|
||||
<script is:inline define:vars={{ siteKey: reCAPTCHA_site_key }}>
|
||||
(function() {
|
||||
const form = document.getElementById('contact-form');
|
||||
if (!form) return;
|
||||
const status = document.getElementById('contact-status');
|
||||
const submit = form.querySelector('button[type="submit"]');
|
||||
const label = document.getElementById('contact-submit-label');
|
||||
const langInput = form.querySelector('input[name="lang"]');
|
||||
const isEn = langInput && langInput.value === 'en';
|
||||
const sendingLabel = isEn ? 'Sending…' : 'Enviando…';
|
||||
const successMsg = isEn ? 'Message sent. We will reply soon.' : 'Mensaje enviado. Te respondemos pronto.';
|
||||
const errorMsg = isEn ? 'Could not send. Please try again or contact us via WhatsApp.' : 'No se pudo enviar. Probá de nuevo o contactanos por WhatsApp.';
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
if (window.grecaptcha && window.grecaptcha.ready) {
|
||||
window.grecaptcha.ready();
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const originalLabel = label.textContent;
|
||||
label.textContent = sendingLabel;
|
||||
submit.disabled = true;
|
||||
status.classList.remove('hidden');
|
||||
status.className = 'text-sm';
|
||||
status.textContent = '';
|
||||
status.style.color = 'var(--hds-fg-soft)';
|
||||
|
||||
try {
|
||||
let token = '';
|
||||
if (window.grecaptcha && window.grecaptcha.execute) {
|
||||
try {
|
||||
token = await Promise.race([
|
||||
window.grecaptcha.execute(siteKey, { action: 'contact' }),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('recaptcha timeout')), 5000)),
|
||||
]);
|
||||
} catch (e) { /* fall through without token */ }
|
||||
}
|
||||
document.getElementById('g-recaptcha-response').value = token || '';
|
||||
|
||||
const formData = new FormData(form);
|
||||
const res = await fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Requested-With': 'fetch' },
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok && data.ok) {
|
||||
status.style.color = 'var(--color-hds-exito)';
|
||||
status.textContent = successMsg;
|
||||
form.reset();
|
||||
} else {
|
||||
throw new Error(data.error || 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
status.style.color = 'var(--color-hds-error)';
|
||||
status.textContent = errorMsg;
|
||||
} finally {
|
||||
label.textContent = originalLabel;
|
||||
submit.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Privacy policy' : 'Política de privacidad'}
|
||||
description={lang === 'en' ? 'Privacy policy of Hosting del Sur.' : 'Política de privacidad de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<article class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-lg">
|
||||
<h1>{lang === 'en' ? 'Privacy policy' : 'Política de privacidad'}</h1>
|
||||
<p class="text-sm text-hds-humo">
|
||||
{lang === 'en' ? 'Last updated' : 'Última actualización'}: 2026
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '1. Data we collect' : '1. Datos que recopilamos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'When you fill out the contact form, we collect: name, email and the message you send us. We use this data exclusively to respond to your inquiry.'
|
||||
: 'Cuando completás el formulario de contacto, recopilamos: nombre, email y el mensaje que nos envías. Usamos estos datos exclusivamente para responder a tu consulta.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '2. How we use your data' : '2. Cómo usamos tus datos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'We use the data you provide solely to: (a) respond to your inquiry, (b) follow up if you become a customer. We do not sell, rent or share your data with third parties.'
|
||||
: 'Usamos los datos que nos proporcionás únicamente para: (a) responder a tu consulta, (b) dar seguimiento si te convertís en cliente. No vendemos, alquilamos ni compartimos tus datos con terceros.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '3. Hosting and data location' : '3. Hosting y ubicación de datos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Our infrastructure is in datacenters in the United States and Europe. If you require data to be hosted in a specific country, please let us know and we will work out the best option.'
|
||||
: 'Nuestra infraestructura se encuentra en datacenters en Estados Unidos y Europa. Si necesitás que los datos se hospeden en un país específico, avísanos y acordamos la mejor opción.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '4. Your rights' : '4. Tus derechos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'You can request access, rectification or deletion of your data at any time by writing to contacto@hostingdelsur.net.'
|
||||
: 'Podés solicitar acceso, rectificación o eliminación de tus datos en cualquier momento escribiendo a contacto@hostingdelsur.net.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '5. Cookies' : '5. Cookies'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'This site does not use tracking cookies. We may use strictly necessary cookies for the language preference.'
|
||||
: 'Este sitio no utiliza cookies de seguimiento. Podemos usar cookies estrictamente necesarias para la preferencia de idioma.'}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Terms of service' : 'Términos del servicio'}
|
||||
description={lang === 'en' ? 'Terms of service of Hosting del Sur.' : 'Términos del servicio de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<article class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-lg">
|
||||
<h1>{lang === 'en' ? 'Terms of service' : 'Términos del servicio'}</h1>
|
||||
<p class="text-sm text-hds-humo">
|
||||
{lang === 'en' ? 'Last updated' : 'Última actualización'}: 2026
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '1. Services' : '1. Servicios'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur provides web hosting, email, web development, e-commerce, content management and IT infrastructure services under individual agreements with each client.'
|
||||
: 'Hosting del Sur brinda servicios de hosting web, correo, desarrollo web, e-commerce, gestión de contenido e infraestructura IT bajo acuerdos individuales con cada cliente.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '2. Acceptable use' : '2. Uso aceptable'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'The client agrees to use the services in accordance with current legislation and the Acceptable Use Policy, refraining from activities that may damage third parties or the integrity of our infrastructure.'
|
||||
: 'El cliente se compromete a utilizar los servicios de conformidad con la legislación vigente y la Política de Uso Aceptable, absteniéndose de realizar actividades que puedan dañar a terceros o la integridad de nuestra infraestructura.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '3. Liability' : '3. Responsabilidad'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur is not liable for damages caused by improper use of the services, force majeure, or third-party actions beyond our control.'
|
||||
: 'Hosting del Sur no se hace responsable por daños causados por uso indebido de los servicios, fuerza mayor o acciones de terceros fuera de nuestro control.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '4. Modifications' : '4. Modificaciones'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'We reserve the right to modify these terms. Clients will be notified with reasonable advance notice.'
|
||||
: 'Nos reservamos el derecho de modificar estos términos. Los clientes serán notificados con antelación razonable.'}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import PricingTiers from '@/components/PricingTiers.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { PLANS, ADDITIONAL_PLANS, monthlyEquivalent } from '@/lib/pricing';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'}
|
||||
description={lang === 'en'
|
||||
? 'Hosting plans in USD, billed annually. Basic, Institutional and E-commerce tiers with optional WordPress, Advanced and Corporate plans.'
|
||||
: 'Planes de hosting en USD, con cobro anual. Básico, Institucional y E-commerce, más planes opcionales WordPress, Avanzado y Corporativo.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
Elegí el plan que se<br/>adapta a tu <span style="color: var(--color-hds-naranja); font-style: italic;">proyecto</span>.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'Prices in USD, billed annually. Features may vary depending on the project and server configuration.'
|
||||
: 'Precios en USD, con cobro anual. Las características pueden variar según el proyecto y la configuración del servidor.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PricingTiers
|
||||
plans={PLANS}
|
||||
whatsappUrl={whatsappUrl}
|
||||
/>
|
||||
|
||||
<div class="mt-10 text-center text-sm space-y-1" style="color: var(--hds-fg-muted);">
|
||||
<p>⚠️ {lang === 'en'
|
||||
? 'Prices are in US Dollars (USD). Billed annually.'
|
||||
: 'Los precios están expresados en dólares estadounidenses (USD). El cobro es anual.'}</p>
|
||||
<p>{lang === 'en'
|
||||
? 'Features may vary based on project and server configuration.'
|
||||
: 'Las características pueden variar según el proyecto y la configuración del servidor.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="font-display mb-4" style="font-size: clamp(1.75rem, 4vw, 2.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Need a different plan?' : '¿Necesitás otro plan?'}
|
||||
</h2>
|
||||
<p class="text-lg mb-8" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en' ? 'We also have ' : 'También tenemos '}
|
||||
<strong>{ADDITIONAL_PLANS.join(', ')}</strong>.
|
||||
{lang === 'en' ? ' Tell us about your project and we will tailor a plan to you.' : ' Charlá con nosotros y te armamos un plan a medida.'}
|
||||
</p>
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-primary">
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
import MailIcon from '@/components/icons/MailIcon.astro';
|
||||
import WordpressIcon from '@/components/icons/WordpressIcon.astro';
|
||||
import CodeIcon from '@/components/icons/CodeIcon.astro';
|
||||
import ShopIcon from '@/components/icons/ShopIcon.astro';
|
||||
import TranslateIcon from '@/components/icons/TranslateIcon.astro';
|
||||
import ServerIcon from '@/components/icons/ServerIcon.astro';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const slug = Astro.params.slug;
|
||||
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{ params: { slug: 'hosting-correo-web' } },
|
||||
{ params: { slug: 'mantenimiento-wordpress' } },
|
||||
{ params: { slug: 'desarrollo-webs-institucionales' } },
|
||||
{ params: { slug: 'e-commerce' } },
|
||||
{ params: { slug: 'gestion-contenido-traducciones' } },
|
||||
{ params: { slug: 'servidores-linux-virtualizacion' } },
|
||||
];
|
||||
}
|
||||
|
||||
const services: Record<string, {
|
||||
icon: any;
|
||||
title_es: string;
|
||||
title_en: string;
|
||||
category: string;
|
||||
body_es: string;
|
||||
body_en: string;
|
||||
features_es: string[];
|
||||
features_en: string[];
|
||||
}> = {
|
||||
'hosting-correo-web': {
|
||||
icon: MailIcon,
|
||||
title_es: 'Hosting correo y web',
|
||||
title_en: 'Hosting for email and web',
|
||||
category: 'Hosting & Correo',
|
||||
body_es: 'Alojamiento web optimizado con cPanel. Infraestructura estable y profesional para que tu sitio rinda al máximo sin complicaciones técnicas. Soporte para WordPress, PHP y MySQL.',
|
||||
body_en: 'Optimized web hosting with cPanel. Stable, professional infrastructure for your site to perform at its best without technical complications. WordPress, PHP and MySQL support.',
|
||||
features_es: [
|
||||
'Panel cPanel intuitivo',
|
||||
'PHP 7.4 a 8.3, MySQL/MariaDB',
|
||||
'SSL gratis (Let\'s Encrypt)',
|
||||
'Backups automáticos diarios',
|
||||
'Soporte para WordPress optimizado',
|
||||
'Correo electrónico corporativo',
|
||||
],
|
||||
features_en: [
|
||||
'Intuitive cPanel',
|
||||
'PHP 7.4 to 8.3, MySQL/MariaDB',
|
||||
'Free SSL (Let\'s Encrypt)',
|
||||
'Daily automatic backups',
|
||||
'Optimized WordPress support',
|
||||
'Corporate email',
|
||||
],
|
||||
},
|
||||
'mantenimiento-wordpress': {
|
||||
icon: WordpressIcon,
|
||||
title_es: 'Mantenimiento WordPress',
|
||||
title_en: 'WordPress maintenance',
|
||||
category: 'Hosting & Correo',
|
||||
body_es: 'Mantenimiento proactivo y parches de seguridad por expertos que participan en el ecosistema oficial de WordPress. Tu sitio, siempre protegido.',
|
||||
body_en: 'Proactive maintenance and security patches by experts participating in the official WordPress ecosystem. Your site, always protected.',
|
||||
features_es: [
|
||||
'Actualizaciones de core, themes y plugins',
|
||||
'Parches de seguridad críticos',
|
||||
'Backups antes de cada cambio',
|
||||
'Monitoreo de uptime 24/7',
|
||||
'Reporte mensual de actividad',
|
||||
'Restauración ante incidentes',
|
||||
],
|
||||
features_en: [
|
||||
'Core, theme and plugin updates',
|
||||
'Critical security patches',
|
||||
'Backups before every change',
|
||||
'24/7 uptime monitoring',
|
||||
'Monthly activity report',
|
||||
'Incident restoration',
|
||||
],
|
||||
},
|
||||
'desarrollo-webs-institucionales': {
|
||||
icon: CodeIcon,
|
||||
title_es: 'Desarrollo webs institucionales',
|
||||
title_en: 'Institutional website development',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Diseño y desarrollo de sitios web corporativos a medida de tu negocio. Trabajamos con foco en performance, accesibilidad y SEO.',
|
||||
body_en: 'Design and development of corporate websites tailored to your business. We focus on performance, accessibility and SEO.',
|
||||
features_es: [
|
||||
'Diseño responsive y accesible (WCAG AA)',
|
||||
'Optimización SEO técnico desde el inicio',
|
||||
'Performance > 90 en Lighthouse',
|
||||
'CMS amigable para edición autónoma',
|
||||
'Formación de uso al equipo del cliente',
|
||||
'Soporte post-lanzamiento',
|
||||
],
|
||||
features_en: [
|
||||
'Responsive, accessible design (WCAG AA)',
|
||||
'Technical SEO from the start',
|
||||
'Lighthouse performance > 90',
|
||||
'Friendly CMS for autonomous editing',
|
||||
'Training for the client team',
|
||||
'Post-launch support',
|
||||
],
|
||||
},
|
||||
'e-commerce': {
|
||||
icon: ShopIcon,
|
||||
title_es: 'Sitios web con e-commerce',
|
||||
title_en: 'E-commerce websites',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Tiendas online robustas con WooCommerce. Pagos seguros, gestión de inventario, envíos y todo lo necesario para vender online.',
|
||||
body_en: 'Robust online stores with WooCommerce. Secure payments, inventory management, shipping and everything you need to sell online.',
|
||||
features_es: [
|
||||
'WooCommerce con pasarelas de pago locales',
|
||||
'Gestión de stock y pedidos',
|
||||
'Integración con transportistas',
|
||||
'Panel de reportes de venta',
|
||||
'Cupones, descuentos y envíos gratis',
|
||||
'Optimización para Core Web Vitals',
|
||||
],
|
||||
features_en: [
|
||||
'WooCommerce with local payment gateways',
|
||||
'Stock and order management',
|
||||
'Carrier integration',
|
||||
'Sales reporting panel',
|
||||
'Coupons, discounts and free shipping',
|
||||
'Core Web Vitals optimization',
|
||||
],
|
||||
},
|
||||
'gestion-contenido-traducciones': {
|
||||
icon: TranslateIcon,
|
||||
title_es: 'Gestión de contenido y traducciones',
|
||||
title_en: 'Content management and translations',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Servicio editorial recurrente: redacción, actualización de contenido, gestión de blog y traducción multilenguaje. Tu sitio siempre al día, sin que tengas que ocuparte.',
|
||||
body_en: 'Recurring editorial service: copywriting, content updates, blog management and multilingual translation. Your site always up to date, without you having to worry.',
|
||||
features_es: [
|
||||
'Redacción profesional de artículos',
|
||||
'Traducción es/en/pt',
|
||||
'Publicación en CMS o WordPress',
|
||||
'Calendario editorial mensual',
|
||||
'Optimización SEO de cada pieza',
|
||||
'Reporte de tráfico y engagement',
|
||||
],
|
||||
features_en: [
|
||||
'Professional article writing',
|
||||
'ES/EN/PT translation',
|
||||
'CMS or WordPress publishing',
|
||||
'Monthly editorial calendar',
|
||||
'SEO optimization of every piece',
|
||||
'Traffic and engagement report',
|
||||
],
|
||||
},
|
||||
'servidores-linux-virtualizacion': {
|
||||
icon: ServerIcon,
|
||||
title_es: 'Servidores Linux y virtualización',
|
||||
title_en: 'Linux servers and virtualization',
|
||||
category: 'Infraestructura',
|
||||
body_es: 'Diseño, instalación y administración de servidores Linux con virtualización KVM. Nubes privadas, servicios empresariales y plataformas de alta disponibilidad.',
|
||||
body_en: 'Design, installation and administration of Linux servers with KVM virtualization. Private clouds, enterprise services and high-availability platforms.',
|
||||
features_es: [
|
||||
'Servidores dedicados o VPS con KVM',
|
||||
'Nubes privadas estilo Nextcloud',
|
||||
'Alta disponibilidad y balanceo',
|
||||
'Monitoreo 24/7 con Prometheus/Grafana',
|
||||
'Respaldos automatizados',
|
||||
'Hardening y seguridad perimetral',
|
||||
],
|
||||
features_en: [
|
||||
'Dedicated servers or VPS with KVM',
|
||||
'Nextcloud-style private clouds',
|
||||
'High availability and balancing',
|
||||
'24/7 monitoring with Prometheus/Grafana',
|
||||
'Automated backups',
|
||||
'Hardening and perimeter security',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const service = services[slug as keyof typeof services];
|
||||
|
||||
if (!service) {
|
||||
return Astro.redirect(lang === 'en' ? '/en/servicios/' : '/servicios/');
|
||||
}
|
||||
|
||||
const title = lang === 'en' ? service.title_en : service.title_es;
|
||||
const body = lang === 'en' ? service.body_en : service.body_es;
|
||||
const features = lang === 'en' ? service.features_en : service.features_es;
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
---
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={body}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="bg-hds-crema py-12 md:py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<nav aria-label="Breadcrumb" class="text-sm text-hds-humo mb-6">
|
||||
<ol class="flex flex-wrap items-center gap-2">
|
||||
<li><a href={`${base}/`} class="hover:text-hds-naranja">{lang === 'en' ? 'Home' : 'Inicio'}</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li><a href={`${base}/servicios/`} class="hover:text-hds-naranja">{lang === 'en' ? 'Services' : 'Servicios'}</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li class="text-hds-grafito" aria-current="page">{title}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-start gap-5 mb-4">
|
||||
<div class="flex-shrink-0 bg-white rounded-lg p-3 border border-hds-linea">
|
||||
<service.icon class="w-10 h-10" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-wider uppercase text-hds-naranja mb-1">
|
||||
{service.category}
|
||||
</p>
|
||||
<h1 class="text-3xl md:text-5xl text-hds-tinta leading-tight">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<p class="text-lg text-hds-grafito leading-relaxed mb-10">
|
||||
{body}
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl md:text-3xl text-hds-tinta mb-6">
|
||||
{lang === 'en' ? 'What is included' : 'Qué incluye'}
|
||||
</h2>
|
||||
|
||||
<ul class="space-y-3 mb-12">
|
||||
{features.map((feature) => (
|
||||
<li class="flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#EE7623" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" aria-hidden="true">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<span class="text-hds-grafito">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div class="bg-hds-crema rounded-lg p-8 border border-hds-linea">
|
||||
<h3 class="text-xl text-hds-tinta mb-2">
|
||||
{lang === 'en' ? 'Want to know more?' : '¿Querés saber más?'}
|
||||
</h3>
|
||||
<p class="text-hds-grafito mb-5">
|
||||
{lang === 'en'
|
||||
? 'Tell us about your project and we will reply with a tailor-made proposal.'
|
||||
: 'Contanos sobre tu proyecto y te respondemos con una propuesta a medida.'}
|
||||
</p>
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center gap-2 bg-hds-naranja hover:bg-hds-naranja-hover text-white text-sm font-semibold px-5 py-2.5 rounded-md transition-colors"
|
||||
>
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import ServiceCard from '@/components/ServiceCard.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
import MailIcon from '@/components/icons/MailIcon.astro';
|
||||
import WordpressIcon from '@/components/icons/WordpressIcon.astro';
|
||||
import CodeIcon from '@/components/icons/CodeIcon.astro';
|
||||
import ShopIcon from '@/components/icons/ShopIcon.astro';
|
||||
import TranslateIcon from '@/components/icons/TranslateIcon.astro';
|
||||
import ServerIcon from '@/components/icons/ServerIcon.astro';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const groups = [
|
||||
{
|
||||
title: t('services.cat.hosting'),
|
||||
items: [
|
||||
{ slug: 'hosting-correo-web', title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web', desc: lang === 'en' ? 'Optimized hosting with cPanel for WordPress, PHP and MySQL.' : 'Alojamiento optimizado con cPanel para WordPress, PHP y MySQL.', Icon: MailIcon, border: 'var(--color-hds-naranja)' },
|
||||
{ slug: 'mantenimiento-wordpress', title: lang === 'en' ? 'WordPress maintenance' : 'Mantenimiento WordPress', desc: lang === 'en' ? 'Security patches and updates by official WordPress ecosystem experts.' : 'Parches de seguridad y actualizaciones por expertos del ecosistema WP.', Icon: WordpressIcon, border: 'var(--color-hds-naranja)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.design'),
|
||||
items: [
|
||||
{ slug: 'desarrollo-webs-institucionales', title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales', desc: lang === 'en' ? 'Corporate sites tailored to your business and identity.' : 'Sitios corporativos a medida de tu negocio e identidad.', Icon: CodeIcon, border: 'var(--color-hds-coral)' },
|
||||
{ slug: 'e-commerce', title: lang === 'en' ? 'E-commerce websites' : 'Sitios web con e-commerce', desc: lang === 'en' ? 'WooCommerce stores with secure payments and inventory.' : 'Tiendas WooCommerce con pagos seguros y gestión de stock.', Icon: ShopIcon, border: 'var(--color-hds-coral)' },
|
||||
{ slug: 'gestion-contenido-traducciones', title: lang === 'en' ? 'Content & translations' : 'Gestión de contenido y traducciones', desc: lang === 'en' ? 'Recurring editorial: copy, updates, blog and translations.' : 'Servicio editorial recurrente: redacción, blog, traducciones.', Icon: TranslateIcon, border: 'var(--color-hds-coral)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.infra'),
|
||||
items: [
|
||||
{ slug: 'servidores-linux-virtualizacion', title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización', desc: lang === 'en' ? 'Linux servers, KVM virtualization, private clouds.' : 'Servidores Linux, virtualización KVM, nubes privadas.', Icon: ServerIcon, border: 'var(--color-hds-durazno)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Services' : 'Servicios'}
|
||||
description={lang === 'en'
|
||||
? 'Hosting, web development, e-commerce, content management and infrastructure services.'
|
||||
: 'Servicios de hosting, desarrollo web, e-commerce, gestión de contenido e infraestructura.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'What we do' : 'Lo que hacemos'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Our' : 'Nuestros'} <span style="color: var(--color-hds-naranja); font-style: italic;">{lang === 'en' ? 'services' : 'servicios'}</span>.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'A complete stack of services to support your business at every step, from basic hosting to complex infrastructure.'
|
||||
: 'Un stack completo de servicios para acompañar a tu empresa en cada paso, desde el hosting básico hasta la infraestructura compleja.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-14">
|
||||
{groups.map((group) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline gap-3 mb-6">
|
||||
<h2 class="text-2xl md:text-3xl font-display" style="color: var(--hds-fg);">{group.title}</h2>
|
||||
<span class="text-xs font-semibold tracking-[0.25em] uppercase" style="color: var(--color-hds-naranja);">
|
||||
{group.items.length} {group.items.length === 1 ? (lang === 'en' ? 'service' : 'servicio') : (lang === 'en' ? 'services' : 'servicios')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{group.items.map((item) => (
|
||||
<ServiceCard
|
||||
href={`${base}/servicios/${item.slug}/`}
|
||||
title={item.title}
|
||||
description={item.desc}
|
||||
Icon={item.Icon}
|
||||
borderColor={item.border}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const categories = [
|
||||
{ key: 'correo', title: lang === 'en' ? 'Email' : 'Correo electrónico', desc: lang === 'en' ? 'Configuration in clients, IMAP/SMTP, webmail.' : 'Configuración en clientes, IMAP/SMTP, webmail.' },
|
||||
{ key: 'wordpress', title: 'WordPress', desc: lang === 'en' ? 'Updates, backups, restoration, security.' : 'Actualizaciones, backups, restauración, seguridad.' },
|
||||
{ key: 'dominios', title: lang === 'en' ? 'Domains & DNS' : 'Dominios y DNS', desc: lang === 'en' ? 'Point a domain, transfers, DNS records.' : 'Apuntar un dominio, transferencias, registros DNS.' },
|
||||
{ key: 'cpanel', title: 'cPanel', desc: lang === 'en' ? 'Email accounts, FTP, databases, files.' : 'Cuentas de correo, FTP, bases de datos, archivos.' },
|
||||
{ key: 'ssl', title: lang === 'en' ? 'SSL certificates' : 'Certificados SSL', desc: lang === 'en' ? 'Install, renewal, force HTTPS.' : 'Instalación, renovación, HTTPS forzado.' },
|
||||
{ key: 'backups', title: lang === 'en' ? 'Backups' : 'Respaldos', desc: lang === 'en' ? 'JetBackup, file and email restoration.' : 'JetBackup, restauración de archivos y correos.' },
|
||||
];
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Tutorials' : 'Instructivos'}
|
||||
description={lang === 'en'
|
||||
? 'Step-by-step guides to the most common questions from Hosting del Sur clients.'
|
||||
: 'Guías paso a paso para las consultas más frecuentes de los clientes de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Knowledge base' : 'Base de conocimiento'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Tutorials' : 'Instructivos'}.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'Step-by-step guides for the questions we receive most often. We are preparing detailed tutorials for each category.'
|
||||
: 'Guías paso a paso para las preguntas que más recibimos. Estamos preparando instructivos detallados para cada categoría.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="rounded-2xl p-6 md:p-8 mb-12 flex flex-col md:flex-row md:items-center md:justify-between gap-6" style="background: linear-gradient(135deg, rgba(238,118,35,0.08) 0%, rgba(250,159,92,0.12) 100%); border: 1px solid rgba(238,118,35,0.25);">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-2" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Coming soon' : 'Próximamente'}
|
||||
</h2>
|
||||
<p style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We are writing detailed tutorials for each category. In the meantime, write to us on WhatsApp and we will help you right away.'
|
||||
: 'Estamos escribiendo instructivos detallados para cada categoría. Mientras tanto, escribinos por WhatsApp y te ayudamos al instante.'}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-primary text-sm flex-shrink-0"
|
||||
>
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl mb-6 font-display" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Categories' : 'Categorías'}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{categories.map((cat) => (
|
||||
<div class="service-card">
|
||||
<h3 class="text-base font-semibold mb-1" style="color: var(--hds-fg);">{cat.title}</h3>
|
||||
<p class="text-sm mb-3" style="color: var(--hds-fg-soft);">{cat.desc}</p>
|
||||
<p class="text-xs italic" style="color: var(--hds-fg-muted);">
|
||||
{lang === 'en' ? 'No tutorials yet' : 'Sin instructivos aún'}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import Hero from '@/components/Hero.astro';
|
||||
import StatsSection from '@/components/StatsSection.astro';
|
||||
import DifferenceCards from '@/components/DifferenceCards.astro';
|
||||
import ServicesSection from '@/components/ServicesSection.astro';
|
||||
import ProcessSteps from '@/components/ProcessSteps.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const description = lang === 'en'
|
||||
? 'Cloud hosting, websites, e-commerce and infrastructure with real human support from Maldonado, Uruguay. Hosting del Sur — technology with a human face.'
|
||||
: 'Alojamiento en la nube, sitios web, e-commerce e infraestructura con soporte humano real desde Maldonado, Uruguay. Hosting del Sur — tecnología con cara de persona.';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Cloud hosting with real human support' : 'Alojamiento en la nube con soporte humano real'}
|
||||
description={description}
|
||||
lang={lang}
|
||||
>
|
||||
<Hero t={t} whatsappUrl={whatsappUrl} />
|
||||
<StatsSection t={t} />
|
||||
<DifferenceCards t={t} />
|
||||
<ServicesSection t={t} lang={lang} />
|
||||
<ProcessSteps t={t} />
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const categories = [
|
||||
{ key: 'correo', title: lang === 'en' ? 'Email' : 'Correo electrónico', desc: lang === 'en' ? 'Configuration in clients, IMAP/SMTP, webmail.' : 'Configuración en clientes, IMAP/SMTP, webmail.' },
|
||||
{ key: 'wordpress', title: 'WordPress', desc: lang === 'en' ? 'Updates, backups, restoration, security.' : 'Actualizaciones, backups, restauración, seguridad.' },
|
||||
{ key: 'dominios', title: lang === 'en' ? 'Domains & DNS' : 'Dominios y DNS', desc: lang === 'en' ? 'Point a domain, transfers, DNS records.' : 'Apuntar un dominio, transferencias, registros DNS.' },
|
||||
{ key: 'cpanel', title: 'cPanel', desc: lang === 'en' ? 'Email accounts, FTP, databases, files.' : 'Cuentas de correo, FTP, bases de datos, archivos.' },
|
||||
{ key: 'ssl', title: lang === 'en' ? 'SSL certificates' : 'Certificados SSL', desc: lang === 'en' ? 'Install, renewal, force HTTPS.' : 'Instalación, renovación, HTTPS forzado.' },
|
||||
{ key: 'backups', title: lang === 'en' ? 'Backups' : 'Respaldos', desc: lang === 'en' ? 'JetBackup, file and email restoration.' : 'JetBackup, restauración de archivos y correos.' },
|
||||
];
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Tutorials' : 'Instructivos'}
|
||||
description={lang === 'en'
|
||||
? 'Step-by-step guides to the most common questions from Hosting del Sur clients.'
|
||||
: 'Guías paso a paso para las consultas más frecuentes de los clientes de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Knowledge base' : 'Base de conocimiento'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Tutorials' : 'Instructivos'}.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'Step-by-step guides for the questions we receive most often. We are preparing detailed tutorials for each category.'
|
||||
: 'Guías paso a paso para las preguntas que más recibimos. Estamos preparando instructivos detallados para cada categoría.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="rounded-2xl p-6 md:p-8 mb-12 flex flex-col md:flex-row md:items-center md:justify-between gap-6" style="background: linear-gradient(135deg, rgba(238,118,35,0.08) 0%, rgba(250,159,92,0.12) 100%); border: 1px solid rgba(238,118,35,0.25);">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold mb-2" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Coming soon' : 'Próximamente'}
|
||||
</h2>
|
||||
<p style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We are writing detailed tutorials for each category. In the meantime, write to us on WhatsApp and we will help you right away.'
|
||||
: 'Estamos escribiendo instructivos detallados para cada categoría. Mientras tanto, escribinos por WhatsApp y te ayudamos al instante.'}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-primary text-sm flex-shrink-0"
|
||||
>
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="text-2xl mb-6 font-display" style="color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Categories' : 'Categorías'}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{categories.map((cat) => (
|
||||
<div class="service-card">
|
||||
<h3 class="text-base font-semibold mb-1" style="color: var(--hds-fg);">{cat.title}</h3>
|
||||
<p class="text-sm mb-3" style="color: var(--hds-fg-soft);">{cat.desc}</p>
|
||||
<p class="text-xs italic" style="color: var(--hds-fg-muted);">
|
||||
{lang === 'en' ? 'No tutorials yet' : 'Sin instructivos aún'}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Privacy policy' : 'Política de privacidad'}
|
||||
description={lang === 'en' ? 'Privacy policy of Hosting del Sur.' : 'Política de privacidad de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<article class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-lg">
|
||||
<h1>{lang === 'en' ? 'Privacy policy' : 'Política de privacidad'}</h1>
|
||||
<p class="text-sm text-hds-humo">
|
||||
{lang === 'en' ? 'Last updated' : 'Última actualización'}: 2026
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '1. Data we collect' : '1. Datos que recopilamos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'When you fill out the contact form, we collect: name, email and the message you send us. We use this data exclusively to respond to your inquiry.'
|
||||
: 'Cuando completás el formulario de contacto, recopilamos: nombre, email y el mensaje que nos envías. Usamos estos datos exclusivamente para responder a tu consulta.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '2. How we use your data' : '2. Cómo usamos tus datos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'We use the data you provide solely to: (a) respond to your inquiry, (b) follow up if you become a customer. We do not sell, rent or share your data with third parties.'
|
||||
: 'Usamos los datos que nos proporcionás únicamente para: (a) responder a tu consulta, (b) dar seguimiento si te convertís en cliente. No vendemos, alquilamos ni compartimos tus datos con terceros.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '3. Hosting and data location' : '3. Hosting y ubicación de datos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Our infrastructure is in datacenters in the United States and Europe. If you require data to be hosted in a specific country, please let us know and we will work out the best option.'
|
||||
: 'Nuestra infraestructura se encuentra en datacenters en Estados Unidos y Europa. Si necesitás que los datos se hospeden en un país específico, avísanos y acordamos la mejor opción.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '4. Your rights' : '4. Tus derechos'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'You can request access, rectification or deletion of your data at any time by writing to contacto@hostingdelsur.net.'
|
||||
: 'Podés solicitar acceso, rectificación o eliminación de tus datos en cualquier momento escribiendo a contacto@hostingdelsur.net.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '5. Cookies' : '5. Cookies'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'This site does not use tracking cookies. We may use strictly necessary cookies for the language preference.'
|
||||
: 'Este sitio no utiliza cookies de seguimiento. Podemos usar cookies estrictamente necesarias para la preferencia de idioma.'}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Terms of service' : 'Términos del servicio'}
|
||||
description={lang === 'en' ? 'Terms of service of Hosting del Sur.' : 'Términos del servicio de Hosting del Sur.'}
|
||||
lang={lang}
|
||||
>
|
||||
<article class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 prose prose-lg">
|
||||
<h1>{lang === 'en' ? 'Terms of service' : 'Términos del servicio'}</h1>
|
||||
<p class="text-sm text-hds-humo">
|
||||
{lang === 'en' ? 'Last updated' : 'Última actualización'}: 2026
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '1. Services' : '1. Servicios'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur provides web hosting, email, web development, e-commerce, content management and IT infrastructure services under individual agreements with each client.'
|
||||
: 'Hosting del Sur brinda servicios de hosting web, correo, desarrollo web, e-commerce, gestión de contenido e infraestructura IT bajo acuerdos individuales con cada cliente.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '2. Acceptable use' : '2. Uso aceptable'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'The client agrees to use the services in accordance with current legislation and the Acceptable Use Policy, refraining from activities that may damage third parties or the integrity of our infrastructure.'
|
||||
: 'El cliente se compromete a utilizar los servicios de conformidad con la legislación vigente y la Política de Uso Aceptable, absteniéndose de realizar actividades que puedan dañar a terceros o la integridad de nuestra infraestructura.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '3. Liability' : '3. Responsabilidad'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur is not liable for damages caused by improper use of the services, force majeure, or third-party actions beyond our control.'
|
||||
: 'Hosting del Sur no se hace responsable por daños causados por uso indebido de los servicios, fuerza mayor o acciones de terceros fuera de nuestro control.'}
|
||||
</p>
|
||||
|
||||
<h2>{lang === 'en' ? '4. Modifications' : '4. Modificaciones'}</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'We reserve the right to modify these terms. Clients will be notified with reasonable advance notice.'
|
||||
: 'Nos reservamos el derecho de modificar estos términos. Los clientes serán notificados con antelación razonable.'}
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import WorldMap from '@/components/icons/WorldMap.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'About us' : 'Nosotros'}
|
||||
description={lang === 'en'
|
||||
? 'Since 2000, Hosting del Sur provides technology services with a human touch from Maldonado, Uruguay, across four countries.'
|
||||
: 'Desde 2000, Hosting del Sur brinda servicios de tecnología con atención humana desde Maldonado, Uruguay, con alcance en cuatro países.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'About us' : 'Nosotros'} />
|
||||
<h1 class="font-display leading-tight mt-5 max-w-4xl" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Technology with a <span style="color: var(--color-hds-naranja); font-style: italic;">human face</span> since 2000.</>
|
||||
: <>Tecnología con <span style="color: var(--color-hds-naranja); font-style: italic;">cara de persona</span> desde 2000.</>}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-16 md:py-20" style="background: var(--hds-bg);">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="prose prose-lg max-w-none" style="color: var(--hds-fg-soft);">
|
||||
<p class="text-lg leading-relaxed" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We are a small team of senior SysAdmins and SREs based in Maldonado, Uruguay. We work with clients across Latin America and the world.'
|
||||
: 'Somos un equipo chico de SysAdmins y SREs senior, radicados en Maldonado, Uruguay. Trabajamos con clientes de toda Latinoamérica y el mundo.'}
|
||||
</p>
|
||||
|
||||
<h2 class="font-display mt-12 mb-4" style="color: var(--hds-fg); font-size: 2rem;">
|
||||
{lang === 'en' ? 'Our story' : 'Nuestra historia'}
|
||||
</h2>
|
||||
<p>
|
||||
{lang === 'en'
|
||||
? 'Hosting del Sur was born in 2000 as a hosting services provider for Uruguayan businesses. From the beginning we were clear: we wanted to offer enterprise-level technology with the closeness of a small team. Twenty-six years later we are still the same — fewer tickets, real names, infrastructure we know by heart.'
|
||||
: 'Hosting del Sur nació en el año 2000 como proveedor de servicios de hosting para empresas uruguayas. Desde el principio teníamos claro: queríamos ofrecer tecnología de nivel enterprise con la cercanía de un equipo chico. Veintiséis años después seguimos siendo lo mismo — menos tickets, nombres reales, infraestructura que conocemos de memoria.'}
|
||||
</p>
|
||||
|
||||
<h2 class="font-display mt-12 mb-4" style="color: var(--hds-fg); font-size: 2rem;">
|
||||
{lang === 'en' ? 'What we believe' : 'En qué creemos'}
|
||||
</h2>
|
||||
<ul class="space-y-2" style="color: var(--hds-fg-soft);">
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'A real person answers.' : 'Una persona real te responde.'}</strong> {lang === 'en' ? 'Always.' : 'Siempre.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'You own your data.' : 'Tus datos son tuyos.'}</strong> {lang === 'en' ? 'We just host it.' : 'Nosotros solo los hospedamos.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Open source first.' : 'Open source primero.'}</strong> {lang === 'en' ? 'Vendor lock-in never.' : 'Vendor lock-in nunca.'}</li>
|
||||
<li><strong style="color: var(--hds-fg);">{lang === 'en' ? 'No magic, no jargon.' : 'Sin magia, sin jerga.'}</strong> {lang === 'en' ? 'We explain what we do.' : 'Explicamos lo que hacemos.'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<SectionEyebrow text={lang === 'en' ? 'Where we work' : 'Dónde trabajamos'} />
|
||||
<h2 class="font-display leading-tight mt-5" style="font-size: clamp(2rem, 4.5vw, 3rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Four countries,<br/><span style="color: var(--color-hds-naranja); font-style: italic;">one team</span>.</>
|
||||
: <>Cuatro países,<br/><span style="color: var(--color-hds-naranja); font-style: italic;">un equipo</span>.</>}
|
||||
</h2>
|
||||
<p class="text-lg mt-5" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'We operate from Maldonado, Uruguay, with infrastructure distributed across four countries: Canada, the United States, Germany and Uruguay. We combine providers in three regions with our own equipment in Uruguay to give you redundancy, low latency and data sovereignty.'
|
||||
: 'Operamos desde Maldonado, Uruguay, con infraestructura distribuida en cuatro países: Canadá, Estados Unidos, Alemania y Uruguay. Combinamos proveedores en tres regiones con equipos propios en Uruguay para darte redundancia, baja latencia y soberanía de datos.'}
|
||||
</p>
|
||||
|
||||
<ul class="mt-8 space-y-3 text-sm" style="color: var(--hds-fg-soft);">
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1.5 w-2 h-2 rounded-full flex-shrink-0" style="background: var(--color-hds-naranja);"></span>
|
||||
<span><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Canada, USA, Germany:' : 'Canadá, USA, Alemania:'}</strong> {lang === 'en' ? 'provider infrastructure.' : 'infraestructura de proveedores.'}</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-1.5 w-2 h-2 rounded-full flex-shrink-0" style="background: var(--color-hds-naranja);"></span>
|
||||
<span><strong style="color: var(--hds-fg);">{lang === 'en' ? 'Uruguay:' : 'Uruguay:'}</strong> {lang === 'en' ? 'our own equipment and headquarters.' : 'equipos propios y sede operativa.'}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl p-6 lg:p-8" style="background: var(--hds-card); border: 1px solid var(--hds-line);">
|
||||
<WorldMap class="w-full h-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import PricingTiers from '@/components/PricingTiers.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { PLANS, ADDITIONAL_PLANS, monthlyEquivalent } from '@/lib/pricing';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'}
|
||||
description={lang === 'en'
|
||||
? 'Hosting plans in USD, billed annually. Basic, Institutional and E-commerce tiers with optional WordPress, Advanced and Corporate plans.'
|
||||
: 'Planes de hosting en USD, con cobro anual. Básico, Institucional y E-commerce, más planes opcionales WordPress, Avanzado y Corporativo.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'Hosting plans' : 'Planes de hosting'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
Elegí el plan que se<br/>adapta a tu <span style="color: var(--color-hds-naranja); font-style: italic;">proyecto</span>.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'Prices in USD, billed annually. Features may vary depending on the project and server configuration.'
|
||||
: 'Precios en USD, con cobro anual. Las características pueden variar según el proyecto y la configuración del servidor.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<PricingTiers
|
||||
plans={PLANS}
|
||||
whatsappUrl={whatsappUrl}
|
||||
/>
|
||||
|
||||
<div class="mt-10 text-center text-sm space-y-1" style="color: var(--hds-fg-muted);">
|
||||
<p>⚠️ {lang === 'en'
|
||||
? 'Prices are in US Dollars (USD). Billed annually.'
|
||||
: 'Los precios están expresados en dólares estadounidenses (USD). El cobro es anual.'}</p>
|
||||
<p>{lang === 'en'
|
||||
? 'Features may vary based on project and server configuration.'
|
||||
: 'Las características pueden variar según el proyecto y la configuración del servidor.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h2 class="font-display mb-4" style="font-size: clamp(1.75rem, 4vw, 2.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Need a different plan?' : '¿Necesitás otro plan?'}
|
||||
</h2>
|
||||
<p class="text-lg mb-8" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en' ? 'We also have ' : 'También tenemos '}
|
||||
<strong>{ADDITIONAL_PLANS.join(', ')}</strong>.
|
||||
{lang === 'en' ? ' Tell us about your project and we will tailor a plan to you.' : ' Charlá con nosotros y te armamos un plan a medida.'}
|
||||
</p>
|
||||
<a href={whatsappUrl} target="_blank" rel="noopener noreferrer" class="btn-primary">
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import FAQAccordion from '@/components/FAQAccordion.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const faqs = lang === 'en' ? [
|
||||
{ q: 'What makes you different from a regular hosting provider?', a: 'A real person answers your call. We are a small team of senior SysAdmins/SREs based in Maldonado, Uruguay. No tickets, no chatbots, no offshore call centers.' },
|
||||
{ q: 'Where is my data physically located?', a: 'We operate infrastructure across four countries: Canada, the United States, Germany and Uruguay. We can work with you to choose the region that best fits your compliance and latency needs.' },
|
||||
{ q: 'Do you offer 24/7 support?', a: 'No. We are available during business hours (Monday to Friday, 9 to 18 UYT). Outside those hours we monitor critical alerts, but human support is provided during business hours.' },
|
||||
{ q: 'Do you manage WordPress sites?', a: 'Yes. We are part of the official WordPress ecosystem. We offer hosting, maintenance, security patches, performance tuning and full development.' },
|
||||
{ q: 'Can you build my company website from scratch?', a: 'Yes. We design and develop institutional sites and e-commerce stores (WooCommerce) with focus on performance, accessibility (WCAG AA) and SEO.' },
|
||||
{ q: 'What if I need a plan that is not listed?', a: 'We have additional tiers (WordPress, Empresarial, Avanzado, Avanzado HD, Corporativo, Corporativo HD). Chat with us on WhatsApp and we will tailor a plan for you.' },
|
||||
{ q: 'Do you provide cloud servers and virtualization?', a: 'Yes. We install and administer Linux servers, KVM virtualization, private clouds (Nextcloud-style), high availability and 24/7 monitoring with Prometheus/Grafana.' },
|
||||
] : [
|
||||
{ q: '¿Qué los diferencia de un proveedor de hosting tradicional?', a: 'Una persona real te atiende. Somos un equipo chico de SysAdmins/SREs senior radicados en Maldonado, Uruguay. Sin tickets, sin chatbots, sin call centers offshore.' },
|
||||
{ q: '¿Dónde están físicamente mis datos?', a: 'Operamos infraestructura en cuatro países: Canadá, Estados Unidos, Alemania y Uruguay. Podemos trabajar con vos para elegir la región que mejor se ajuste a tus necesidades de compliance y latencia.' },
|
||||
{ q: '¿Tienen soporte 24/7?', a: 'No. Estamos disponibles en horario comercial (lunes a viernes, 9 a 18 hs UYT). Fuera de ese horario monitoreamos alertas críticas, pero el soporte humano se brinda en horario comercial.' },
|
||||
{ q: '¿Manejan sitios WordPress?', a: 'Sí. Somos parte del ecosistema oficial de WordPress. Ofrecemos hosting, mantenimiento, parches de seguridad, optimización de performance y desarrollo completo.' },
|
||||
{ q: '¿Pueden armar el sitio web de mi empresa desde cero?', a: 'Sí. Diseñamos y desarrollamos sitios institucionales y e-commerce (WooCommerce) con foco en performance, accesibilidad (WCAG AA) y SEO.' },
|
||||
{ q: '¿Qué pasa si necesito un plan que no está en la lista?', a: 'Tenemos tiers adicionales (WordPress, Empresarial, Avanzado, Avanzado HD, Corporativo, Corporativo HD). Charlá con nosotros por WhatsApp y te armamos un plan a medida.' },
|
||||
{ q: '¿Brindan servidores cloud y virtualización?', a: 'Sí. Instalamos y administramos servidores Linux, virtualización KVM, nubes privadas (estilo Nextcloud), alta disponibilidad y monitoreo 24/7 con Prometheus/Grafana.' },
|
||||
];
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'FAQ' : 'Preguntas frecuentes'}
|
||||
description={lang === 'en'
|
||||
? 'Frequently asked questions about Hosting del Sur, our plans, infrastructure and support.'
|
||||
: 'Preguntas frecuentes sobre Hosting del Sur, nuestros planes, infraestructura y soporte.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'FAQ' : 'Preguntas frecuentes'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en'
|
||||
? <>Common <span style="color: var(--color-hds-naranja); font-style: italic;">questions</span>.</>
|
||||
: <>Preguntas <span style="color: var(--color-hds-naranja); font-style: italic;">frecuentes</span>.</>}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<FAQAccordion items={faqs} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,268 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
import MailIcon from '@/components/icons/MailIcon.astro';
|
||||
import WordpressIcon from '@/components/icons/WordpressIcon.astro';
|
||||
import CodeIcon from '@/components/icons/CodeIcon.astro';
|
||||
import ShopIcon from '@/components/icons/ShopIcon.astro';
|
||||
import TranslateIcon from '@/components/icons/TranslateIcon.astro';
|
||||
import ServerIcon from '@/components/icons/ServerIcon.astro';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const slug = Astro.params.slug;
|
||||
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{ params: { slug: 'hosting-correo-web' } },
|
||||
{ params: { slug: 'mantenimiento-wordpress' } },
|
||||
{ params: { slug: 'desarrollo-webs-institucionales' } },
|
||||
{ params: { slug: 'e-commerce' } },
|
||||
{ params: { slug: 'gestion-contenido-traducciones' } },
|
||||
{ params: { slug: 'servidores-linux-virtualizacion' } },
|
||||
];
|
||||
}
|
||||
|
||||
const services: Record<string, {
|
||||
icon: any;
|
||||
title_es: string;
|
||||
title_en: string;
|
||||
category: string;
|
||||
body_es: string;
|
||||
body_en: string;
|
||||
features_es: string[];
|
||||
features_en: string[];
|
||||
}> = {
|
||||
'hosting-correo-web': {
|
||||
icon: MailIcon,
|
||||
title_es: 'Hosting correo y web',
|
||||
title_en: 'Hosting for email and web',
|
||||
category: 'Hosting & Correo',
|
||||
body_es: 'Alojamiento web optimizado con cPanel. Infraestructura estable y profesional para que tu sitio rinda al máximo sin complicaciones técnicas. Soporte para WordPress, PHP y MySQL.',
|
||||
body_en: 'Optimized web hosting with cPanel. Stable, professional infrastructure for your site to perform at its best without technical complications. WordPress, PHP and MySQL support.',
|
||||
features_es: [
|
||||
'Panel cPanel intuitivo',
|
||||
'PHP 7.4 a 8.3, MySQL/MariaDB',
|
||||
'SSL gratis (Let\'s Encrypt)',
|
||||
'Backups automáticos diarios',
|
||||
'Soporte para WordPress optimizado',
|
||||
'Correo electrónico corporativo',
|
||||
],
|
||||
features_en: [
|
||||
'Intuitive cPanel',
|
||||
'PHP 7.4 to 8.3, MySQL/MariaDB',
|
||||
'Free SSL (Let\'s Encrypt)',
|
||||
'Daily automatic backups',
|
||||
'Optimized WordPress support',
|
||||
'Corporate email',
|
||||
],
|
||||
},
|
||||
'mantenimiento-wordpress': {
|
||||
icon: WordpressIcon,
|
||||
title_es: 'Mantenimiento WordPress',
|
||||
title_en: 'WordPress maintenance',
|
||||
category: 'Hosting & Correo',
|
||||
body_es: 'Mantenimiento proactivo y parches de seguridad por expertos que participan en el ecosistema oficial de WordPress. Tu sitio, siempre protegido.',
|
||||
body_en: 'Proactive maintenance and security patches by experts participating in the official WordPress ecosystem. Your site, always protected.',
|
||||
features_es: [
|
||||
'Actualizaciones de core, themes y plugins',
|
||||
'Parches de seguridad críticos',
|
||||
'Backups antes de cada cambio',
|
||||
'Monitoreo de uptime 24/7',
|
||||
'Reporte mensual de actividad',
|
||||
'Restauración ante incidentes',
|
||||
],
|
||||
features_en: [
|
||||
'Core, theme and plugin updates',
|
||||
'Critical security patches',
|
||||
'Backups before every change',
|
||||
'24/7 uptime monitoring',
|
||||
'Monthly activity report',
|
||||
'Incident restoration',
|
||||
],
|
||||
},
|
||||
'desarrollo-webs-institucionales': {
|
||||
icon: CodeIcon,
|
||||
title_es: 'Desarrollo webs institucionales',
|
||||
title_en: 'Institutional website development',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Diseño y desarrollo de sitios web corporativos a medida de tu negocio. Trabajamos con foco en performance, accesibilidad y SEO.',
|
||||
body_en: 'Design and development of corporate websites tailored to your business. We focus on performance, accessibility and SEO.',
|
||||
features_es: [
|
||||
'Diseño responsive y accesible (WCAG AA)',
|
||||
'Optimización SEO técnico desde el inicio',
|
||||
'Performance > 90 en Lighthouse',
|
||||
'CMS amigable para edición autónoma',
|
||||
'Formación de uso al equipo del cliente',
|
||||
'Soporte post-lanzamiento',
|
||||
],
|
||||
features_en: [
|
||||
'Responsive, accessible design (WCAG AA)',
|
||||
'Technical SEO from the start',
|
||||
'Lighthouse performance > 90',
|
||||
'Friendly CMS for autonomous editing',
|
||||
'Training for the client team',
|
||||
'Post-launch support',
|
||||
],
|
||||
},
|
||||
'e-commerce': {
|
||||
icon: ShopIcon,
|
||||
title_es: 'Sitios web con e-commerce',
|
||||
title_en: 'E-commerce websites',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Tiendas online robustas con WooCommerce. Pagos seguros, gestión de inventario, envíos y todo lo necesario para vender online.',
|
||||
body_en: 'Robust online stores with WooCommerce. Secure payments, inventory management, shipping and everything you need to sell online.',
|
||||
features_es: [
|
||||
'WooCommerce con pasarelas de pago locales',
|
||||
'Gestión de stock y pedidos',
|
||||
'Integración con transportistas',
|
||||
'Panel de reportes de venta',
|
||||
'Cupones, descuentos y envíos gratis',
|
||||
'Optimización para Core Web Vitals',
|
||||
],
|
||||
features_en: [
|
||||
'WooCommerce with local payment gateways',
|
||||
'Stock and order management',
|
||||
'Carrier integration',
|
||||
'Sales reporting panel',
|
||||
'Coupons, discounts and free shipping',
|
||||
'Core Web Vitals optimization',
|
||||
],
|
||||
},
|
||||
'gestion-contenido-traducciones': {
|
||||
icon: TranslateIcon,
|
||||
title_es: 'Gestión de contenido y traducciones',
|
||||
title_en: 'Content management and translations',
|
||||
category: 'Diseño & Contenido',
|
||||
body_es: 'Servicio editorial recurrente: redacción, actualización de contenido, gestión de blog y traducción multilenguaje. Tu sitio siempre al día, sin que tengas que ocuparte.',
|
||||
body_en: 'Recurring editorial service: copywriting, content updates, blog management and multilingual translation. Your site always up to date, without you having to worry.',
|
||||
features_es: [
|
||||
'Redacción profesional de artículos',
|
||||
'Traducción es/en/pt',
|
||||
'Publicación en CMS o WordPress',
|
||||
'Calendario editorial mensual',
|
||||
'Optimización SEO de cada pieza',
|
||||
'Reporte de tráfico y engagement',
|
||||
],
|
||||
features_en: [
|
||||
'Professional article writing',
|
||||
'ES/EN/PT translation',
|
||||
'CMS or WordPress publishing',
|
||||
'Monthly editorial calendar',
|
||||
'SEO optimization of every piece',
|
||||
'Traffic and engagement report',
|
||||
],
|
||||
},
|
||||
'servidores-linux-virtualizacion': {
|
||||
icon: ServerIcon,
|
||||
title_es: 'Servidores Linux y virtualización',
|
||||
title_en: 'Linux servers and virtualization',
|
||||
category: 'Infraestructura',
|
||||
body_es: 'Diseño, instalación y administración de servidores Linux con virtualización KVM. Nubes privadas, servicios empresariales y plataformas de alta disponibilidad.',
|
||||
body_en: 'Design, installation and administration of Linux servers with KVM virtualization. Private clouds, enterprise services and high-availability platforms.',
|
||||
features_es: [
|
||||
'Servidores dedicados o VPS con KVM',
|
||||
'Nubes privadas estilo Nextcloud',
|
||||
'Alta disponibilidad y balanceo',
|
||||
'Monitoreo 24/7 con Prometheus/Grafana',
|
||||
'Respaldos automatizados',
|
||||
'Hardening y seguridad perimetral',
|
||||
],
|
||||
features_en: [
|
||||
'Dedicated servers or VPS with KVM',
|
||||
'Nextcloud-style private clouds',
|
||||
'High availability and balancing',
|
||||
'24/7 monitoring with Prometheus/Grafana',
|
||||
'Automated backups',
|
||||
'Hardening and perimeter security',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const service = services[slug as keyof typeof services];
|
||||
|
||||
if (!service) {
|
||||
return Astro.redirect(lang === 'en' ? '/en/servicios/' : '/servicios/');
|
||||
}
|
||||
|
||||
const title = lang === 'en' ? service.title_en : service.title_es;
|
||||
const body = lang === 'en' ? service.body_en : service.body_es;
|
||||
const features = lang === 'en' ? service.features_en : service.features_es;
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
---
|
||||
<BaseLayout
|
||||
title={title}
|
||||
description={body}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="bg-hds-crema py-12 md:py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<nav aria-label="Breadcrumb" class="text-sm text-hds-humo mb-6">
|
||||
<ol class="flex flex-wrap items-center gap-2">
|
||||
<li><a href={`${base}/`} class="hover:text-hds-naranja">{lang === 'en' ? 'Home' : 'Inicio'}</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li><a href={`${base}/servicios/`} class="hover:text-hds-naranja">{lang === 'en' ? 'Services' : 'Servicios'}</a></li>
|
||||
<li aria-hidden="true">/</li>
|
||||
<li class="text-hds-grafito" aria-current="page">{title}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-start gap-5 mb-4">
|
||||
<div class="flex-shrink-0 bg-white rounded-lg p-3 border border-hds-linea">
|
||||
<service.icon class="w-10 h-10" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-semibold tracking-wider uppercase text-hds-naranja mb-1">
|
||||
{service.category}
|
||||
</p>
|
||||
<h1 class="text-3xl md:text-5xl text-hds-tinta leading-tight">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-16 md:py-20 bg-hds-marfil">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<p class="text-lg text-hds-grafito leading-relaxed mb-10">
|
||||
{body}
|
||||
</p>
|
||||
|
||||
<h2 class="text-2xl md:text-3xl text-hds-tinta mb-6">
|
||||
{lang === 'en' ? 'What is included' : 'Qué incluye'}
|
||||
</h2>
|
||||
|
||||
<ul class="space-y-3 mb-12">
|
||||
{features.map((feature) => (
|
||||
<li class="flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#EE7623" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mt-0.5" aria-hidden="true">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<span class="text-hds-grafito">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div class="bg-hds-crema rounded-lg p-8 border border-hds-linea">
|
||||
<h3 class="text-xl text-hds-tinta mb-2">
|
||||
{lang === 'en' ? 'Want to know more?' : '¿Querés saber más?'}
|
||||
</h3>
|
||||
<p class="text-hds-grafito mb-5">
|
||||
{lang === 'en'
|
||||
? 'Tell us about your project and we will reply with a tailor-made proposal.'
|
||||
: 'Contanos sobre tu proyecto y te respondemos con una propuesta a medida.'}
|
||||
</p>
|
||||
<a
|
||||
href={whatsappUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center justify-center gap-2 bg-hds-naranja hover:bg-hds-naranja-hover text-white text-sm font-semibold px-5 py-2.5 rounded-md transition-colors"
|
||||
>
|
||||
{lang === 'en' ? 'Chat on WhatsApp' : 'Hablar por WhatsApp'}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow.astro';
|
||||
import ServiceCard from '@/components/ServiceCard.astro';
|
||||
import CtaFinal from '@/components/CtaFinal.astro';
|
||||
import { getLangFromUrl, useTranslations } from '@/i18n/utils';
|
||||
import MailIcon from '@/components/icons/MailIcon.astro';
|
||||
import WordpressIcon from '@/components/icons/WordpressIcon.astro';
|
||||
import CodeIcon from '@/components/icons/CodeIcon.astro';
|
||||
import ShopIcon from '@/components/icons/ShopIcon.astro';
|
||||
import TranslateIcon from '@/components/icons/TranslateIcon.astro';
|
||||
import ServerIcon from '@/components/icons/ServerIcon.astro';
|
||||
|
||||
const lang = getLangFromUrl(Astro.url);
|
||||
const t = useTranslations(lang);
|
||||
const base = lang === 'en' ? '/en' : '';
|
||||
const whatsappUrl = 'https://wa.me/59899812487';
|
||||
|
||||
const groups = [
|
||||
{
|
||||
title: t('services.cat.hosting'),
|
||||
items: [
|
||||
{ slug: 'hosting-correo-web', title: lang === 'en' ? 'Hosting for email & web' : 'Hosting correo y web', desc: lang === 'en' ? 'Optimized hosting with cPanel for WordPress, PHP and MySQL.' : 'Alojamiento optimizado con cPanel para WordPress, PHP y MySQL.', Icon: MailIcon, border: 'var(--color-hds-naranja)' },
|
||||
{ slug: 'mantenimiento-wordpress', title: lang === 'en' ? 'WordPress maintenance' : 'Mantenimiento WordPress', desc: lang === 'en' ? 'Security patches and updates by official WordPress ecosystem experts.' : 'Parches de seguridad y actualizaciones por expertos del ecosistema WP.', Icon: WordpressIcon, border: 'var(--color-hds-naranja)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.design'),
|
||||
items: [
|
||||
{ slug: 'desarrollo-webs-institucionales', title: lang === 'en' ? 'Institutional websites' : 'Desarrollo webs institucionales', desc: lang === 'en' ? 'Corporate sites tailored to your business and identity.' : 'Sitios corporativos a medida de tu negocio e identidad.', Icon: CodeIcon, border: 'var(--color-hds-coral)' },
|
||||
{ slug: 'e-commerce', title: lang === 'en' ? 'E-commerce websites' : 'Sitios web con e-commerce', desc: lang === 'en' ? 'WooCommerce stores with secure payments and inventory.' : 'Tiendas WooCommerce con pagos seguros y gestión de stock.', Icon: ShopIcon, border: 'var(--color-hds-coral)' },
|
||||
{ slug: 'gestion-contenido-traducciones', title: lang === 'en' ? 'Content & translations' : 'Gestión de contenido y traducciones', desc: lang === 'en' ? 'Recurring editorial: copy, updates, blog and translations.' : 'Servicio editorial recurrente: redacción, blog, traducciones.', Icon: TranslateIcon, border: 'var(--color-hds-coral)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('services.cat.infra'),
|
||||
items: [
|
||||
{ slug: 'servidores-linux-virtualizacion', title: lang === 'en' ? 'Linux servers & virtualization' : 'Servidores Linux y virtualización', desc: lang === 'en' ? 'Linux servers, KVM virtualization, private clouds.' : 'Servidores Linux, virtualización KVM, nubes privadas.', Icon: ServerIcon, border: 'var(--color-hds-durazno)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
<BaseLayout
|
||||
title={lang === 'en' ? 'Services' : 'Servicios'}
|
||||
description={lang === 'en'
|
||||
? 'Hosting, web development, e-commerce, content management and infrastructure services.'
|
||||
: 'Servicios de hosting, desarrollo web, e-commerce, gestión de contenido e infraestructura.'}
|
||||
lang={lang}
|
||||
>
|
||||
<section class="pt-20 pb-12 md:pt-28 md:pb-16" style="background: var(--hds-bg-soft);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<SectionEyebrow text={lang === 'en' ? 'What we do' : 'Lo que hacemos'} />
|
||||
<h1 class="font-display leading-tight mt-5" style="font-size: clamp(2.5rem, 6vw, 4.5rem); color: var(--hds-fg);">
|
||||
{lang === 'en' ? 'Our' : 'Nuestros'} <span style="color: var(--color-hds-naranja); font-style: italic;">{lang === 'en' ? 'services' : 'servicios'}</span>.
|
||||
</h1>
|
||||
<p class="text-lg mt-5 max-w-2xl" style="color: var(--hds-fg-soft);">
|
||||
{lang === 'en'
|
||||
? 'A complete stack of services to support your business at every step, from basic hosting to complex infrastructure.'
|
||||
: 'Un stack completo de servicios para acompañar a tu empresa en cada paso, desde el hosting básico hasta la infraestructura compleja.'}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="py-20 md:py-24" style="background: var(--hds-bg);">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-14">
|
||||
{groups.map((group) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline gap-3 mb-6">
|
||||
<h2 class="text-2xl md:text-3xl font-display" style="color: var(--hds-fg);">{group.title}</h2>
|
||||
<span class="text-xs font-semibold tracking-[0.25em] uppercase" style="color: var(--color-hds-naranja);">
|
||||
{group.items.length} {group.items.length === 1 ? (lang === 'en' ? 'service' : 'servicio') : (lang === 'en' ? 'services' : 'servicios')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{group.items.map((item) => (
|
||||
<ServiceCard
|
||||
href={`${base}/servicios/${item.slug}/`}
|
||||
title={item.title}
|
||||
description={item.desc}
|
||||
Icon={item.Icon}
|
||||
borderColor={item.border}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CtaFinal t={t} whatsappUrl={whatsappUrl} />
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,233 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Arista Pro Alternate';
|
||||
src: url('/fonts/Arista-Pro-Alternate-Regular.woff2') format('woff2'),
|
||||
url('/fonts/Arista-Pro-Alternate-Regular.woff') format('woff');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@import "@fontsource/mulish/400.css";
|
||||
@import "@fontsource/mulish/500.css";
|
||||
@import "@fontsource/mulish/600.css";
|
||||
@import "@fontsource/mulish/700.css";
|
||||
@import "@fontsource/cormorant-garamond/400.css";
|
||||
@import "@fontsource/cormorant-garamond/500.css";
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@theme {
|
||||
--color-hds-naranja: #EE7623;
|
||||
--color-hds-naranja-hover: #D8651A;
|
||||
--color-hds-coral: #FA9F5C;
|
||||
--color-hds-durazno: #FFCBA1;
|
||||
--color-hds-tinta: #1A1612;
|
||||
|
||||
--font-display: 'Arista Pro Alternate', 'Mulish', system-ui, sans-serif;
|
||||
--font-sans: 'Mulish', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-editorial: 'Cormorant Garamond', Georgia, serif;
|
||||
|
||||
--tracking-display: -0.02em;
|
||||
--tracking-eyebrow: 0.25em;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--hds-bg: #FAF6EE;
|
||||
--hds-bg-soft: #F5EFE3;
|
||||
--hds-bg-section: #FFFFFF;
|
||||
--hds-fg: #1A1612;
|
||||
--hds-fg-soft: #4A4540;
|
||||
--hds-fg-muted: #8A8580;
|
||||
--hds-line: #E8E2D5;
|
||||
--hds-card: #FFFFFF;
|
||||
--hds-card-soft: #FFFFFF;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
color-scheme: dark;
|
||||
--hds-bg: #1A1612;
|
||||
--hds-bg-soft: #221E1A;
|
||||
--hds-bg-section: #2A2520;
|
||||
--hds-fg: #FAF6EE;
|
||||
--hds-fg-soft: #C8C2B8;
|
||||
--hds-fg-muted: #8A8580;
|
||||
--hds-line: #3A332C;
|
||||
--hds-card: #2A2520;
|
||||
--hds-card-soft: #221E1A;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
color: var(--hds-fg);
|
||||
background-color: var(--hds-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 400;
|
||||
letter-spacing: var(--tracking-display);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.font-display { font-family: var(--font-display); }
|
||||
.font-editorial { font-family: var(--font-editorial); }
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: var(--tracking-eyebrow);
|
||||
text-transform: uppercase;
|
||||
color: var(--color-hds-naranja);
|
||||
}
|
||||
|
||||
.eyebrow::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1px;
|
||||
background: var(--color-hds-naranja);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-hds-naranja);
|
||||
color: #FFFFFF;
|
||||
padding: 0.875rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
border: 2px solid var(--color-hds-naranja);
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: var(--color-hds-naranja-hover);
|
||||
border-color: var(--color-hds-naranja-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
border: 2px solid var(--hds-fg);
|
||||
color: var(--hds-fg);
|
||||
padding: 0.78rem 1.4rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.2s ease;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: var(--hds-fg);
|
||||
color: var(--hds-bg);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
color: var(--color-hds-naranja);
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
transition: gap 0.2s ease;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid var(--color-hds-naranja);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-hds-naranja);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background: var(--hds-card);
|
||||
border: 1px solid var(--hds-line);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
.service-card:hover {
|
||||
border-color: var(--color-hds-naranja);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 32px rgba(238,118,35,0.08);
|
||||
}
|
||||
|
||||
.particle-grid {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(238,118,35,0.10) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(238,118,35,0.10) 1px, transparent 1px);
|
||||
background-size: 56px 56px;
|
||||
}
|
||||
|
||||
:root.dark .particle-grid {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(238,118,35,0.18) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(238,118,35,0.18) 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 1rem;
|
||||
z-index: 9999;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--color-hds-naranja);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: top 0.2s ease;
|
||||
}
|
||||
.skip-link:focus { top: 1rem; }
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-hds-naranja);
|
||||
outline-offset: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user