Avvia i18n reale home IT EN TH e corregge rendering locale #8

Merged
javaxman merged 4 commits from feat/issue-5-i18n-home-menu-footer into main 2026-04-27 10:20:29 +02:00
18 changed files with 1200 additions and 412 deletions

View File

@ -7,10 +7,7 @@ CONTACT_FORM_MODE=dev
CONTACT_FORM_RESEND_API_KEY= CONTACT_FORM_RESEND_API_KEY=
CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.com CONTACT_FORM_FROM_EMAIL=no-reply@nexstudio.com
CONTACT_FORM_TO_EMAIL=info@nexstudio.com CONTACT_FORM_TO_EMAIL=info@nexstudio.com
CONTACT_FORM_TO_EMAIL_SUPPORT=support@nexstudio.com CONTACT_FORM_TO_EMAIL_INFO=info@nexstudio.com
CONTACT_FORM_TO_EMAIL_PRIVACY= CONTACT_FORM_TO_EMAIL_PRIVACY=
CONTACT_FORM_TO_EMAIL_SECURITY= CONTACT_FORM_TO_EMAIL_CAREERS=
CONTACT_FORM_TO_EMAIL_ETHICS=
CONTACT_FORM_TO_EMAIL_LEGAL=
CONTACT_FORM_TO_EMAIL_HR=
CONTACT_TURNSTILE_SECRET_KEY= CONTACT_TURNSTILE_SECRET_KEY=

183
README.md
View File

@ -1,104 +1,123 @@
# Astro Starter Kit: Minimal # NexStudio Website Runbook
```sh Sito istituzionale NexStudio basato su Astro, con deploy su Cloudflare e form contatti protetto da Turnstile.
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! ## Obiettivo del repository
## 🚀 Project Structure - Presentazione aziendale e servizi (homepage e pagine legali).
- Entry point unico per richieste commerciali e supporto.
- Base pronta per contenuti multilingua IT/EN/TH.
- Pipeline di deploy stabile su Cloudflare.
Inside of your Astro project, you'll see the following folders and files: ## Stack tecnico
```text - Astro + Tailwind CSS
/ - Adapter Cloudflare (`@astrojs/cloudflare`)
├── public/ - Sitemap (`@astrojs/sitemap`)
├── src/ - Form backend su endpoint `/api/contact`
│ └── pages/ - Turnstile + Resend per anti-spam e invio email
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. ## Struttura essenziale
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. - `src/pages/`: pagine pubbliche e route API (`src/pages/api/contact.ts`)
- `src/components/`: componenti UI condivisi
- `src/data/`: contenuti testuali e configurazioni pagine
- `public/`: asset statici (loghi, immagini, robots)
- `.env.example`: variabili locali di riferimento
Any static assets, like images, can be placed in the `public/` directory. ## Prerequisiti
## 🧞 Commands - Node.js 20+
- npm 10+
- Account Cloudflare (Pages/Workers + Turnstile)
- Account Resend (API key + sender verificato)
All commands are run from the root of the project, from a terminal: ## Avvio locale
| Command | Action | 1. Installazione dipendenze:
| :------------------------ | :----------------------------------------------- | - `npm install`
| `npm install` | Installs dependencies | 2. Copia variabili di ambiente:
| `npm run dev` | Starts local dev server at `localhost:4321` | - copia `.env.example` in `.env`
| `npm run build` | Build your production site to `./dist/` | 3. Avvio sviluppo:
| `npm run preview` | Preview your build locally, before deploying | - `npm run dev`
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 4. Se Vite resta in cache incoerente:
| `npm run astro -- --help` | Get help using the Astro CLI | - `npm run dev:fresh`
## 👀 Want to learn more? ## Comandi utili
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). - `npm run dev`: sviluppo locale
- `npm run dev:fresh`: reset cache `.vite` e `.astro` + avvio dev server
- `npm run build`: build produzione
- `npm run preview`: preview locale della build
- `npm run deploy:cf`: build + deploy con wrangler su output Astro
## Cloudflare deploy notes ## Configurazione contatti
If you deploy with Wrangler in CI/Cloudflare, build first and deploy using Astro's generated Worker config: Variabili pubbliche (frontend):
- `PUBLIC_CONTACT_FORM_ACTION_URL` (default consigliato: `/api/contact`)
- `PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT` (URL assoluto della pagina post-submit)
- `PUBLIC_CONTACT_FORM_MAILTO` (fallback se action URL non disponibile)
- `PUBLIC_CONTACT_TURNSTILE_SITE_KEY`
Variabili server (Cloudflare/local):
- `CONTACT_FORM_MODE` (`dev` oppure `live`)
- `CONTACT_FORM_RESEND_API_KEY`
- `CONTACT_FORM_FROM_EMAIL` (consigliato: `no-reply@nexstudio.ai`)
- `CONTACT_FORM_TO_EMAIL` (default destinatario, es. `info@nexstudio.ai`)
- `CONTACT_FORM_TO_EMAIL_INFO` (opzionale, override esplicito per area "info")
- `CONTACT_FORM_TO_EMAIL_PRIVACY`
- `CONTACT_FORM_TO_EMAIL_CAREERS`
- `CONTACT_TURNSTILE_SECRET_KEY`
Comportamento:
- `dev`: accetta submit e scrive payload nei log (senza invio reale)
- `live`: valida Turnstile e invia email via Resend
Routing pubblico consigliato (portale vetrina):
- `info`: informazioni generali/commerciali
- `privacy`: richieste privacy e diritti interessato
- `careers`: candidature e collaborazioni
Indirizzi consigliati portale:
- `info@nexstudio.ai`
- `privacy@nexstudio.ai`
- `careers@nexstudio.ai`
- `no-reply@nexstudio.ai` (solo invio comunicazioni/newsletter, non destinazione inbound)
## Deploy Cloudflare
Impostazioni consigliate:
- Build command: `npm run build` - Build command: `npm run build`
- Deploy command: `npm run deploy:cf` - Deploy command: `npm run deploy:cf`
This avoids entrypoint errors like `@astrojs/cloudflare/entrypoints/server` not found. Questa configurazione evita errori tipo:
`The entry-point file at "@astrojs/cloudflare/entrypoints/server" was not found.`
## Contact form configuration ## Dominio e sicurezza
Set these variables for the website contact form. - Dominio ufficiale: `https://nexstudio.ai`
- Hostname Turnstile da mantenere allineati:
Public (frontend):
- `PUBLIC_CONTACT_FORM_ACTION_URL` (default `/api/contact`)
- `PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT` (absolute URL for `_next`)
- `PUBLIC_CONTACT_FORM_MAILTO` (fallback only if `ACTION_URL` is empty)
- `PUBLIC_CONTACT_TURNSTILE_SITE_KEY`
Server-side (Cloudflare runtime / local `.env`):
- `CONTACT_FORM_MODE` (`dev` or `live`)
- `CONTACT_FORM_RESEND_API_KEY`
- `CONTACT_FORM_FROM_EMAIL` (verified sender in Resend, recommended `no-reply@nexstudio.com`)
- `CONTACT_FORM_TO_EMAIL` (default recipient inbox or alias, recommended `info@nexstudio.com`)
- `CONTACT_FORM_TO_EMAIL_SUPPORT`
- `CONTACT_FORM_TO_EMAIL_PRIVACY`
- `CONTACT_FORM_TO_EMAIL_SECURITY`
- `CONTACT_FORM_TO_EMAIL_ETHICS`
- `CONTACT_FORM_TO_EMAIL_LEGAL`
- `CONTACT_FORM_TO_EMAIL_HR`
- `CONTACT_TURNSTILE_SECRET_KEY`
Mode behavior:
- `dev`: accepts form and logs payload server-side (no real email send)
- `live`: verifies Turnstile (if secret configured) and sends via Resend API
Department routing:
- the contact form includes a department selector
- if a department-specific env is set, the email is routed to that recipient
- otherwise it falls back to `CONTACT_FORM_TO_EMAIL`
Suggested operational use:
- `info@...` for prospects and generic clarifications (non-clients)
- `support@...` for existing customers
- `no-reply@...` as sender for newsletters and automated communications
Copy `.env.example` to `.env` for local development and configure the same keys in Cloudflare Pages/Workers for production.
## Production domain
- Official domain: `https://nexstudio.ai`
- Keep Turnstile widget hostnames aligned with:
- `nexstudio.ai` - `nexstudio.ai`
- `www.nexstudio.ai` (if used) - `www.nexstudio.ai` (se usato)
- `localhost` (for local testing) - `localhost` (test locale)
- Accesso pre-go-live gestito con Cloudflare Zero Trust Access
## Stato operativo attuale
- Issue #1 (contact form live): implementata
- Issue #2 (anti-spam Turnstile): implementata
- Issue #3 (SEO base): implementata (sitemap, robots, canonical/OG/Twitter)
- Issue #4: in avanzamento (contenuti non placeholder)
- Issue #5: da completare (localizzazione reale IT/EN/TH)
## Note operative
- Per modifiche non minori usare branch dedicato (`feat/*`, `fix/*`) e merge su `main` dopo verifica.
- Evitare commit su `main` per attivita complesse.
- Rigenerare eventuali API key esposte e aggiornare subito i secret su Cloudflare.

View File

@ -1,5 +1,10 @@
--- ---
import { cta } from '../data/home'; import { getCta } from '../data/home';
const path = Astro.url.pathname;
const localeMatch = path.match(/^\/(en|th)(\/|$)/);
const currentLocale = (localeMatch?.[1] ?? 'it') as 'it' | 'en' | 'th';
const cta = getCta(currentLocale);
const cf = cta.contactForm; const cf = cta.contactForm;
const model = { const model = {
@ -14,6 +19,15 @@ const model = {
fieldDepartment: cf.fieldDepartment, fieldDepartment: cf.fieldDepartment,
fieldEmail: cf.fieldEmail, fieldEmail: cf.fieldEmail,
fieldMessage: cf.fieldMessage, fieldMessage: cf.fieldMessage,
turnstileError: cf.errors.turnstile,
configError: cf.errors.config,
mailtoFallbackSubject: cf.mailtoFallbackSubject,
mailtoLabelName: cf.mailtoLabels.name,
mailtoLabelCompany: cf.mailtoLabels.company,
mailtoLabelEmail: cf.mailtoLabels.email,
mailtoLabelCountry: cf.mailtoLabels.country,
mailtoLabelDepartment: cf.mailtoLabels.department,
mailtoDefaultDepartment: cf.mailtoLabels.defaultDepartment,
}; };
const hasFormspree = const hasFormspree =
typeof cf.actionUrl === 'string' && cf.actionUrl.length > 0; typeof cf.actionUrl === 'string' && cf.actionUrl.length > 0;
@ -203,11 +217,11 @@ const fieldClass =
class="cf-turnstile" class="cf-turnstile"
data-sitekey={cf.turnstileSiteKey} data-sitekey={cf.turnstileSiteKey}
data-theme="dark" data-theme="dark"
data-language="it" data-language={cf.turnstileLanguage}
data-size="normal" data-size="normal"
></div> ></div>
<p class="mt-1.5 text-xs leading-relaxed text-nx-muted"> <p class="mt-1.5 text-xs leading-relaxed text-nx-muted">
Verifica anti-spam: completa il controllo prima di inviare il messaggio. {cf.turnstileHint}
</p> </p>
</div> </div>
) : null ) : null

View File

@ -1,5 +1,10 @@
--- ---
import { footer } from '../data/home'; import { getFooter } from '../data/home';
const path = Astro.url.pathname;
const localeMatch = path.match(/^\/(en|th)(\/|$)/);
const currentLocale = (localeMatch?.[1] ?? 'it') as 'it' | 'en' | 'th';
const footer = getFooter(currentLocale);
const hasNewsletterAction = const hasNewsletterAction =
typeof footer.newsletter.actionUrl === 'string' && typeof footer.newsletter.actionUrl === 'string' &&
@ -17,9 +22,6 @@ const notifySubject =
const linkClass = const linkClass =
'text-sm text-sky-400/90 transition-colors hover:text-sky-300'; 'text-sm text-sky-400/90 transition-colors hover:text-sky-300';
const path = Astro.url.pathname;
const localeMatch = path.match(/^\/(en|th)(\/|$)/);
const currentLocale = localeMatch?.[1] ?? 'it';
const withLocale = (href: string) => { const withLocale = (href: string) => {
if (currentLocale === 'it') return href; if (currentLocale === 'it') return href;
if (href.startsWith(`/${currentLocale}`)) return href; if (href.startsWith(`/${currentLocale}`)) return href;
@ -91,7 +93,7 @@ const withLocale = (href: string) => {
) : ( ) : (
<span <span
class="cursor-not-allowed text-nx-muted opacity-50" class="cursor-not-allowed text-nx-muted opacity-50"
title="URL da configurare in footer.ts" title={footer.labels.socialPlaceholderTitle}
> >
{item.label} {item.label}
</span> </span>
@ -115,7 +117,7 @@ const withLocale = (href: string) => {
{link.label} {link.label}
</a> </a>
) : ( ) : (
<span class="text-sm text-nx-muted" title="Collegamento in arrivo"> <span class="text-sm text-nx-muted" title={footer.labels.companyPlaceholderTitle}>
{link.label} {link.label}
</span> </span>
)} )}
@ -131,7 +133,7 @@ const withLocale = (href: string) => {
> >
<div class="min-w-0"> <div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-wider text-nx-fg"> <p class="text-xs font-semibold uppercase tracking-wider text-nx-fg">
Impostazioni cookie {footer.labels.cookieTitle}
</p> </p>
<p class="mt-1.5 max-w-xl text-xs leading-relaxed text-nx-muted"> <p class="mt-1.5 max-w-xl text-xs leading-relaxed text-nx-muted">
{footer.cookiePreferences.hint} {footer.cookiePreferences.hint}
@ -204,7 +206,7 @@ const withLocale = (href: string) => {
{nl.buttonLabel} {nl.buttonLabel}
</button> </button>
<p id="nx-newsletter-hint" class="text-xs leading-relaxed text-nx-muted"> <p id="nx-newsletter-hint" class="text-xs leading-relaxed text-nx-muted">
Iscrizione in arrivo: configura lendpoint nel progetto per attivare il modulo. {footer.labels.newsletterComingSoon}
</p> </p>
</div> </div>
) )

View File

@ -5,48 +5,48 @@ const contactSuccessRedirect = (env.PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT ?? '').
const contactMailto = (env.PUBLIC_CONTACT_FORM_MAILTO ?? '').trim(); const contactMailto = (env.PUBLIC_CONTACT_FORM_MAILTO ?? '').trim();
const contactTurnstileSiteKey = (env.PUBLIC_CONTACT_TURNSTILE_SITE_KEY ?? '').trim(); const contactTurnstileSiteKey = (env.PUBLIC_CONTACT_TURNSTILE_SITE_KEY ?? '').trim();
export const cta = { import type { SupportedLocale } from './navigation';
/** Riga sezione (stesso ruolo di FAQ, Stack, …) */
eyebrow: 'Contatti',
/** Titolo CTA su due righe (dopo il punto) */ const ctaByLocale = {
it: {
eyebrow: 'Contatti',
titleLine1: 'Automatizza il complesso.', titleLine1: 'Automatizza il complesso.',
titleLine2: 'Velocizza il processo.', titleLine2: 'Velocizza il processo.',
body: body:
'Dai modelli LLM alla distribuzione globale in pochi millisecondi. La directory definitiva per il LegalTech e MedTech. Utilizza il modulo contatto qui a fianco e ti forniremo tutti i chiarimenti che ti servono.', 'Dai modelli LLM alla distribuzione globale in pochi millisecondi. La directory definitiva per il LegalTech e MedTech. Utilizza il modulo contatto qui a fianco e ti forniremo tutti i chiarimenti che ti servono.',
/**
* Modulo contatti in pagina (sezione Contatti).
*
* **Formspree:** crea un form su https://formspree.io e incolla lURL in `actionUrl`.
* **Solo mailto:** lascia `actionUrl` vuoto e imposta `mailto` con lindirizzo destinatario
* (si apre il client di posta dellutente).
*/
contactForm: { contactForm: {
/** Lasciare vuoti endpoint/chiavi nelle fasi di sola UI. */
title: 'Parliamone', title: 'Parliamone',
description: description:
'Lascia i tuoi dati e il messaggio: ti risponde il team NexStudio.', 'Lascia i tuoi dati e il messaggio: ti risponde il team NexStudio.',
nameLabel: 'Nome', nameLabel: 'Nome',
companyLabel: 'Azienda (opzionale)', companyLabel: 'Azienda (opzionale)',
countryLabel: 'Paese', countryLabel: 'Paese',
departmentLabel: 'Reparto', departmentLabel: 'Area',
emailLabel: 'Email', emailLabel: 'Email',
messageLabel: 'Scrivi qui il tuo messaggio', messageLabel: 'Scrivi qui il tuo messaggio',
submitLabel: 'Invia messaggio', submitLabel: 'Invia messaggio',
/** Formspree: `https://formspree.io/f/xxxx` — valorizzato via `PUBLIC_CONTACT_FORM_ACTION_URL` */
actionUrl: contactActionUrl, actionUrl: contactActionUrl,
/** Formspree `_next` — URL assoluta in produzione (`PUBLIC_CONTACT_FORM_SUCCESS_REDIRECT`) */
successRedirect: contactSuccessRedirect, successRedirect: contactSuccessRedirect,
/**
* Cloudflare Turnstile (anti-spam): inserisci qui la site key pubblica.
* Se vuota, il CAPTCHA non viene mostrato.
*/
turnstileSiteKey: contactTurnstileSiteKey, turnstileSiteKey: contactTurnstileSiteKey,
/** Formspree `_subject` e oggetto mailto */ turnstileLanguage: 'it',
turnstileHint:
'Verifica anti-spam: completa il controllo prima di inviare il messaggio.',
notifySubject: 'Richiesta contatto — sito NexStudio', notifySubject: 'Richiesta contatto — sito NexStudio',
/** Fallback se `actionUrl` e' vuoto (`PUBLIC_CONTACT_FORM_MAILTO`) */
mailto: contactMailto, mailto: contactMailto,
mailtoFallbackSubject: 'Contatto sito',
mailtoLabels: {
name: 'Nome',
company: 'Azienda',
email: 'Email',
country: 'Paese',
department: 'Area',
defaultDepartment: 'general',
},
errors: {
turnstile: 'Completa il controllo anti-spam prima di inviare il messaggio.',
config:
'Configura `contactForm.actionUrl` (Formspree) o `contactForm.mailto` in `src/data/home/cta.ts`.',
},
fieldName: 'name', fieldName: 'name',
fieldCompany: 'company', fieldCompany: 'company',
fieldCountry: 'country', fieldCountry: 'country',
@ -54,13 +54,116 @@ export const cta = {
fieldEmail: 'email', fieldEmail: 'email',
fieldMessage: 'message', fieldMessage: 'message',
departmentOptions: [ departmentOptions: [
{ value: 'general', label: 'Generale' }, { value: 'info', label: 'Informazioni generali e commerciali' },
{ value: 'support', label: 'Support' }, { value: 'privacy', label: 'Privacy' },
{ value: 'privacy', label: 'Privacy / DPO' }, { value: 'careers', label: 'Careers / Collaborazioni' },
{ value: 'security', label: 'Security / Incident Response' },
{ value: 'ethics', label: 'Segnalazioni confidenziali (Ethics)' },
{ value: 'legal', label: 'Legal' },
{ value: 'hr', label: 'HR' },
], ],
}, },
},
en: {
eyebrow: 'Contacts',
titleLine1: 'Automate complexity.',
titleLine2: 'Accelerate operations.',
body:
'From LLM models to global distribution in milliseconds. Use the contact form and our team will provide all the details you need.',
contactForm: {
title: 'Lets talk',
description: 'Share your details and message: the NexStudio team will reply.',
nameLabel: 'Name',
companyLabel: 'Company (optional)',
countryLabel: 'Country',
departmentLabel: 'Area',
emailLabel: 'Email',
messageLabel: 'Write your message',
submitLabel: 'Send message',
actionUrl: contactActionUrl,
successRedirect: contactSuccessRedirect,
turnstileSiteKey: contactTurnstileSiteKey,
turnstileLanguage: 'en',
turnstileHint:
'Anti-spam verification: complete the challenge before sending your message.',
notifySubject: 'Contact request — NexStudio website',
mailto: contactMailto,
mailtoFallbackSubject: 'Website contact',
mailtoLabels: {
name: 'Name',
company: 'Company',
email: 'Email',
country: 'Country',
department: 'Area',
defaultDepartment: 'general',
},
errors: {
turnstile: 'Complete the anti-spam check before sending your message.',
config:
'Configure `contactForm.actionUrl` (Formspree) or `contactForm.mailto` in `src/data/home/cta.ts`.',
},
fieldName: 'name',
fieldCompany: 'company',
fieldCountry: 'country',
fieldDepartment: 'department',
fieldEmail: 'email',
fieldMessage: 'message',
departmentOptions: [
{ value: 'info', label: 'General and commercial information' },
{ value: 'privacy', label: 'Privacy' },
{ value: 'careers', label: 'Careers / Collaborations' },
],
},
},
th: {
eyebrow: 'ติดต่อ',
titleLine1: 'ทำงานซับซ้อนให้อัตโนมัติ',
titleLine2: 'เร่งความเร็วกระบวนการ',
body:
'จากโมเดล LLM สู่การกระจายระบบระดับโลกภายในไม่กี่มิลลิวินาที ใช้ฟอร์มติดต่อด้านข้าง แล้วทีม NexStudio จะตอบกลับพร้อมข้อมูลที่คุณต้องการ',
contactForm: {
title: 'คุยกับเรา',
description: 'กรอกข้อมูลและข้อความ แล้วทีม NexStudio จะติดต่อกลับ',
nameLabel: 'ชื่อ',
companyLabel: 'บริษัท (ไม่บังคับ)',
countryLabel: 'ประเทศ',
departmentLabel: 'แผนก',
emailLabel: 'อีเมล',
messageLabel: 'พิมพ์ข้อความของคุณ',
submitLabel: 'ส่งข้อความ',
actionUrl: contactActionUrl,
successRedirect: contactSuccessRedirect,
turnstileSiteKey: contactTurnstileSiteKey,
turnstileLanguage: 'th',
turnstileHint: 'การยืนยันป้องกันสแปม: กรุณาทำขั้นตอนให้เสร็จก่อนส่งข้อความ',
notifySubject: 'คำขอติดต่อ — เว็บไซต์ NexStudio',
mailto: contactMailto,
mailtoFallbackSubject: 'ติดต่อจากเว็บไซต์',
mailtoLabels: {
name: 'ชื่อ',
company: 'บริษัท',
email: 'อีเมล',
country: 'ประเทศ',
department: 'แผนก',
defaultDepartment: 'general',
},
errors: {
turnstile: 'กรุณายืนยันการป้องกันสแปมก่อนส่งข้อความ',
config:
'กรุณาตั้งค่า `contactForm.actionUrl` (Formspree) หรือ `contactForm.mailto` ใน `src/data/home/cta.ts`',
},
fieldName: 'name',
fieldCompany: 'company',
fieldCountry: 'country',
fieldDepartment: 'department',
fieldEmail: 'email',
fieldMessage: 'message',
departmentOptions: [
{ value: 'info', label: 'ข้อมูลทั่วไปและเชิงพาณิชย์' },
{ value: 'privacy', label: 'ความเป็นส่วนตัว' },
{ value: 'careers', label: 'Careers / Collaborations' },
],
},
},
} as const; } as const;
export const getCta = (locale: SupportedLocale) => ctaByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const cta = ctaByLocale.it;

View File

@ -1,5 +1,8 @@
import type { SupportedLocale } from './navigation';
/** FAQ — domande/risposte allineate a hero, prodotti, servizi e stack */ /** FAQ — domande/risposte allineate a hero, prodotti, servizi e stack */
export const faq = { const faqByLocale = {
it: {
eyebrow: 'FAQ', eyebrow: 'FAQ',
title: 'Domande frequenti', title: 'Domande frequenti',
items: [ items: [
@ -44,4 +47,102 @@ export const faq = {
'Il punto di ingresso principale è la chat dal pulsante «Chattiamo» in header: prima vedi se il canale è con operatore umano, solo chatbot o misto, poi lasci email (e facoltativamente il nome) per ricevere un link o un codice monouso e aprire la sessione senza perdite di tempo. In chat operatore umano e chatbot collaborano per orientamento iniziale, requisiti e passi successivi. Per il supporto via web ci interfacciamo in italiano, inglese, tedesco, francese, spagnolo o tailandese. Per materiale sensibile useremo sempre i canali concordati in fase di rapporto commerciale. Se ci si sente più a proprio agio usando un form di contatto, in fondo alla homepage nella sezione Contatti trovate il modulo a destra del testo introduttivo.', 'Il punto di ingresso principale è la chat dal pulsante «Chattiamo» in header: prima vedi se il canale è con operatore umano, solo chatbot o misto, poi lasci email (e facoltativamente il nome) per ricevere un link o un codice monouso e aprire la sessione senza perdite di tempo. In chat operatore umano e chatbot collaborano per orientamento iniziale, requisiti e passi successivi. Per il supporto via web ci interfacciamo in italiano, inglese, tedesco, francese, spagnolo o tailandese. Per materiale sensibile useremo sempre i canali concordati in fase di rapporto commerciale. Se ci si sente più a proprio agio usando un form di contatto, in fondo alla homepage nella sezione Contatti trovate il modulo a destra del testo introduttivo.',
}, },
] as const, ] as const,
},
en: {
eyebrow: 'FAQ',
title: 'Frequently asked questions',
items: [
{
question: 'What organizations do you support with LexAura and MediAura?',
answer:
'We work with law firms and healthcare organizations, from solo professionals to multi-site teams, that manage high information volumes and need reliable tools in regulated environments.',
},
{
question: 'Does AI replace lawyers or doctors?',
answer:
'No. Our products support professional workflows with summarization, research, assisted drafting, and automation. Final decisions and responsibility always remain with the professional.',
},
{
question: 'What happens after software adoption?',
answer:
'Our service model goes beyond licensing: specialist onboarding and training, continuous technical support and consulting, and ongoing engine updates aligned with regulations and market context.',
},
{
question: 'Can you integrate LexAura or MediAura with our existing systems?',
answer:
'Yes, when needed. We support API integrations and custom modules within our SaaS perimeter to connect existing management or document flows.',
},
{
question: 'How do you handle security, availability, and sensitive data?',
answer:
'We design for high-availability cloud-native architectures, encryption, perimeter protection, and operational continuity, with clear data-governance practices aligned with applicable regulations.',
},
{
question: 'Do you work only locally or also with international clients?',
answer:
'We are based in Bangkok and collaborate internationally. Workshops can be remote or on-site depending on the project.',
},
{
question: 'How do you handle software usage rights and ownership?',
answer:
'This depends on the contract. In many engagements, exclusive rights on deliverables are transferred after final payment, with explicit clauses for open source and third-party components.',
},
{
question: 'What is the fastest way to talk with your team?',
answer:
'The primary entry point is the chat button in the header. You provide your email to receive a one-time link or code, then start the session without delay.',
},
] as const,
},
th: {
eyebrow: 'คำถามที่พบบ่อย',
title: 'คำถามที่พบบ่อย',
items: [
{
question: 'องค์กรแบบไหนที่เหมาะกับ LexAura และ MediAura?',
answer:
'เราทำงานกับสำนักงานกฎหมายและองค์กรสุขภาพ ตั้งแต่ผู้เชี่ยวชาญเดี่ยวไปจนถึงทีมหลายสาขา ที่ต้องจัดการข้อมูลจำนวนมากในบริบทที่มีข้อกำกับสูง',
},
{
question: 'AI จะแทนที่ทนายหรือแพทย์หรือไม่?',
answer:
'ไม่ใช่ เป้าหมายของเราคือช่วยงานเชิงวิเคราะห์ งานเอกสาร และเวิร์กโฟลว์ซ้ำ ๆ โดยการตัดสินใจสุดท้ายยังคงเป็นหน้าที่ของผู้เชี่ยวชาญ',
},
{
question: 'หลังเริ่มใช้งานซอฟต์แวร์แล้วจะเกิดอะไรขึ้น?',
answer:
'เรามีบริการหลังการใช้งานครบวงจร ทั้ง onboarding การอบรมเฉพาะทาง การสนับสนุนทางเทคนิค และการอัปเดตระบบอย่างต่อเนื่อง',
},
{
question: 'สามารถเชื่อม LexAura หรือ MediAura กับระบบเดิมของเราได้ไหม?',
answer:
'ได้ เรารองรับการเชื่อมต่อผ่าน API และโมดูลแบบกำหนดเองภายในขอบเขต SaaS ของเรา',
},
{
question: 'จัดการความปลอดภัย ความพร้อมใช้งาน และข้อมูลอ่อนไหวอย่างไร?',
answer:
'เราใช้สถาปัตยกรรมคลาวด์ที่พร้อมใช้งานสูง การเข้ารหัส การป้องกันระดับเครือข่าย และแนวปฏิบัติด้านการกำกับข้อมูลให้สอดคล้องข้อกำกับที่เกี่ยวข้อง',
},
{
question: 'รับงานเฉพาะในพื้นที่หรือรับลูกค้าต่างประเทศด้วย?',
answer:
'สำนักงานใหญ่ปฏิบัติการของเราอยู่ที่กรุงเทพฯ และทำงานกับลูกค้าต่างประเทศได้ ทั้งออนไลน์และออนไซต์ตามความเหมาะสมของโครงการ',
},
{
question: 'สิทธิการใช้งานและความเป็นเจ้าของซอฟต์แวร์กำหนดอย่างไร?',
answer:
'ขึ้นกับสัญญา โดยทั่วไปจะระบุเงื่อนไขการใช้งาน ความรับผิดชอบ และสิทธิในชิ้นงานอย่างชัดเจน รวมถึงส่วนประกอบโอเพนซอร์สและ third-party',
},
{
question: 'ถ้าต้องการคุยเร็วที่สุดควรติดต่อช่องทางไหน?',
answer:
'กดปุ่มแชทบนส่วนหัวเว็บไซต์ ใส่อีเมลเพื่อรับลิงก์หรือโค้ดแบบใช้ครั้งเดียว แล้วเริ่มเซสชันได้ทันที',
},
] as const,
},
} as const; } as const;
export const getFaq = (locale: SupportedLocale) => faqByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const faq = faqByLocale.it;

View File

@ -1,87 +1,178 @@
import type { SupportedLocale } from './navigation';
/** Footer — griglia 4 colonne (link); newsletter in blocco unico sotto, allineata a sinistra */ /** Footer — griglia 4 colonne (link); newsletter in blocco unico sotto, allineata a sinistra */
export const footer = { const footerByLocale = {
it: {
brandName: 'NexStudio', brandName: 'NexStudio',
tagline: 'Ingegneria del software e IA per settori regolamentati.', tagline: 'Ingegneria del software e IA per settori regolamentati.',
legalLine: 'P.IVA e note legali: da inserire.', legalLine: 'P.IVA e note legali: da inserire.',
columnTitles: { columnTitles: {
sections: 'Sezioni', sections: 'Sezioni',
legal: 'Normative', legal: 'Normative',
social: 'Reti sociali', social: 'Reti sociali',
company: 'Azienda', company: 'Azienda',
} as const, } as const,
/** Colonna 1 — ancore sulla homepage */
homeNav: [ homeNav: [
{ label: 'Prodotti', href: '/#prodotti' }, { label: 'Prodotti', href: '/#prodotti' },
{ label: 'Servizi', href: '/#servizi' }, { label: 'Servizi', href: '/#servizi' },
{ label: 'Stack', href: '/#stack' }, { label: 'Stack', href: '/#stack' },
{ label: 'FAQ', href: '/#faq' }, { label: 'FAQ', href: '/#faq' },
] as const, ] as const,
/** Pannello preferenze (CMP): non è un documento legale; tenuto fuori dalla colonna Normative */
cookiePreferences: { cookiePreferences: {
label: 'Impostazioni cookie', label: 'Impostazioni cookie',
hint: 'Preferenze di consenso sul tuo browser (non è un testo legale).', hint: 'Preferenze di consenso sul tuo browser (non è un testo legale).',
} as const, } as const,
/** Colonna 2 — documenti legali (testi bozza nelle rispettive pagine) */
legalColumnNav: [ legalColumnNav: [
{ label: 'Condizioni duso', href: '/terms' }, { label: 'Condizioni duso', href: '/terms' },
{ label: 'Informativa cookie', href: '/cookies' }, { label: 'Informativa cookie', href: '/cookies' },
{ label: 'Privacy', href: '/privacy' }, { label: 'Privacy', href: '/privacy' },
{ label: 'Diritti privacy (GDPR)', href: '/gdpr' }, { label: 'Diritti privacy (GDPR)', href: '/gdpr' },
] as const, ] as const,
/**
* Colonna 3 — social (sostituire gli URL con i profili reali).
* Lasciare `href` vuoto per mostrare solo etichetta disabilitata finché non configurato.
*/
socialLinks: [ socialLinks: [
{ label: 'Facebook', href: '' }, { label: 'Facebook', href: '' },
{ label: 'LinkedIn', href: '' }, { label: 'LinkedIn', href: '' },
{ label: 'Twitter / X', href: '' }, { label: 'Twitter / X', href: '' },
{ label: 'GitHub', href: '' }, { label: 'GitHub', href: '' },
] as const, ] as const,
/** Colonna 4 — pagine istituzionali e compliance */
companyNav: [ companyNav: [
{ label: 'Chi siamo', href: '/about' }, { label: 'Chi siamo', href: '/about' },
{ label: 'Codice etico', href: '/codice-etico' }, { label: 'Codice etico', href: '/codice-etico' },
{ label: 'Modello organizzativo', href: '/modello-organizzativo' }, { label: 'Modello organizzativo', href: '/modello-organizzativo' },
{ label: 'Dove siamo', href: '/dove-siamo' }, { label: 'Dove siamo', href: '/dove-siamo' },
] as const, ] as const,
/**
* Newsletter — invio via POST a un endpoint esterno.
*
* Opzione A (consigliata per partire): Formspree
* 1. Crea account su https://formspree.io e un nuovo form.
* 2. Copia lURL tipo `https://formspree.io/f/abcdefgh` in `actionUrl`.
* 3. Opzionale: `successRedirect` = URL assoluta della pagina di ringraziamento
* (es. `https://tuodominio.com/newsletter/grazie` — vedi `src/pages/newsletter/grazie.astro`).
* 4. In Formspree collega linvio a email / Slack / Google Sheets.
*
* Opzione B: Brevo, Mailchimp, Buttondown, ConvertKit — usano spesso un URL
* dedicato o un embed; adatta `fieldName` al nome campo richiesto dalla doc.
*
* Opzione C: Worker Cloudflare + lista in KV/D1 (più controllo, più codice).
*/
newsletter: { newsletter: {
title: 'Newsletter', title: 'Newsletter',
description: 'Aggiornamenti su prodotti e roadmap.', description: 'Aggiornamenti su prodotti e roadmap.',
placeholder: 'La tua email', placeholder: 'La tua email',
buttonLabel: 'Iscriviti', buttonLabel: 'Iscriviti',
/** Formspree: `https://formspree.io/f/xxxx` — vuoto = modulo disabilitato */
actionUrl: '', actionUrl: '',
/** Nome campo email richiesto dal provider (Formspree: di solito `email`) */
fieldName: 'email', fieldName: 'email',
/**
* Formspree: redirect dopo invio (`_next`). Deve essere URL **assoluta**
* (es. produzione). Vuoto = pagina di ringrazio predefinita Formspree.
*/
successRedirect: '', successRedirect: '',
/** Formspree: oggetto della mail di notifica (`_subject`) */
notifySubject: 'Nuova iscrizione newsletter — NexStudio', notifySubject: 'Nuova iscrizione newsletter — NexStudio',
}, },
labels: {
cookieTitle: 'Impostazioni cookie',
socialPlaceholderTitle: 'URL da configurare in footer.ts',
companyPlaceholderTitle: 'Collegamento in arrivo',
newsletterComingSoon:
'Iscrizione in arrivo: configura lendpoint nel progetto per attivare il modulo.',
},
},
en: {
brandName: 'NexStudio',
tagline: 'Software engineering and AI for regulated industries.',
legalLine: 'VAT number and legal notes: to be added.',
columnTitles: {
sections: 'Sections',
legal: 'Legal',
social: 'Social',
company: 'Company',
} as const,
homeNav: [
{ label: 'Products', href: '/#prodotti' },
{ label: 'Services', href: '/#servizi' },
{ label: 'Stack', href: '/#stack' },
{ label: 'FAQ', href: '/#faq' },
] as const,
cookiePreferences: {
label: 'Cookie settings',
hint: 'Consent preferences in your browser (this is not a legal document).',
} as const,
legalColumnNav: [
{ label: 'Terms of use', href: '/terms' },
{ label: 'Cookie policy', href: '/cookies' },
{ label: 'Privacy', href: '/privacy' },
{ label: 'Privacy rights (GDPR)', href: '/gdpr' },
] as const,
socialLinks: [
{ label: 'Facebook', href: '' },
{ label: 'LinkedIn', href: '' },
{ label: 'Twitter / X', href: '' },
{ label: 'GitHub', href: '' },
] as const,
companyNav: [
{ label: 'About us', href: '/about' },
{ label: 'Code of ethics', href: '/codice-etico' },
{ label: 'Organizational model', href: '/modello-organizzativo' },
{ label: 'Where we are', href: '/dove-siamo' },
] as const,
newsletter: {
title: 'Newsletter',
description: 'Updates on products and roadmap.',
placeholder: 'Your email',
buttonLabel: 'Subscribe',
actionUrl: '',
fieldName: 'email',
successRedirect: '',
notifySubject: 'New newsletter signup — NexStudio',
},
labels: {
cookieTitle: 'Cookie settings',
socialPlaceholderTitle: 'URL to configure in footer.ts',
companyPlaceholderTitle: 'Coming soon',
newsletterComingSoon:
'Newsletter signups coming soon: configure the endpoint in the project to enable this form.',
},
},
th: {
brandName: 'NexStudio',
tagline: 'วิศวกรรมซอฟต์แวร์และ AI สำหรับอุตสาหกรรมที่มีข้อกำกับ',
legalLine: 'เลขประจำตัวผู้เสียภาษีและข้อมูลทางกฎหมาย: รอเพิ่มภายหลัง',
columnTitles: {
sections: 'ส่วนต่าง ๆ',
legal: 'กฎหมาย',
social: 'โซเชียล',
company: 'บริษัท',
} as const,
homeNav: [
{ label: 'ผลิตภัณฑ์', href: '/#prodotti' },
{ label: 'บริการ', href: '/#servizi' },
{ label: 'สแตก', href: '/#stack' },
{ label: 'คำถามที่พบบ่อย', href: '/#faq' },
] as const,
cookiePreferences: {
label: 'ตั้งค่าคุกกี้',
hint: 'การตั้งค่าความยินยอมในเบราว์เซอร์ของคุณ (ไม่ใช่เอกสารทางกฎหมาย)',
} as const,
legalColumnNav: [
{ label: 'เงื่อนไขการใช้งาน', href: '/terms' },
{ label: 'นโยบายคุกกี้', href: '/cookies' },
{ label: 'ความเป็นส่วนตัว', href: '/privacy' },
{ label: 'สิทธิความเป็นส่วนตัว (GDPR)', href: '/gdpr' },
] as const,
socialLinks: [
{ label: 'Facebook', href: '' },
{ label: 'LinkedIn', href: '' },
{ label: 'Twitter / X', href: '' },
{ label: 'GitHub', href: '' },
] as const,
companyNav: [
{ label: 'เกี่ยวกับเรา', href: '/about' },
{ label: 'จรรยาบรรณ', href: '/codice-etico' },
{ label: 'โมเดลองค์กร', href: '/modello-organizzativo' },
{ label: 'ที่ตั้ง', href: '/dove-siamo' },
] as const,
newsletter: {
title: 'จดหมายข่าว',
description: 'อัปเดตเกี่ยวกับผลิตภัณฑ์และโรดแมป',
placeholder: 'อีเมลของคุณ',
buttonLabel: 'สมัครรับข่าวสาร',
actionUrl: '',
fieldName: 'email',
successRedirect: '',
notifySubject: 'มีผู้สมัครจดหมายข่าวใหม่ — NexStudio',
},
labels: {
cookieTitle: 'ตั้งค่าคุกกี้',
socialPlaceholderTitle: 'ต้องตั้งค่า URL ใน footer.ts',
companyPlaceholderTitle: 'กำลังจะเปิดใช้งาน',
newsletterComingSoon:
'ระบบสมัครจดหมายข่าวกำลังจะเปิดใช้งาน: ตั้งค่า endpoint ในโปรเจกต์เพื่อเปิดฟอร์มนี้',
},
},
} as const; } as const;
export const getFooter = (locale: SupportedLocale) => footerByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const footer = footerByLocale.it;

View File

@ -1,14 +1,14 @@
import type { SupportedLocale } from './navigation';
/** Sezione hero */ /** Sezione hero */
export const hero = { const heroByLocale = {
it: {
badge: 'Restituiamo valore al tuo tempo', badge: 'Restituiamo valore al tuo tempo',
/** Parte prima dellevidenziazione nel titolo */
titleBefore: "Ingegneria del software per l'era dell'", titleBefore: "Ingegneria del software per l'era dell'",
/** Segmento con gradiente nel titolo */
titleHighlight: 'Intelligenza Artificiale', titleHighlight: 'Intelligenza Artificiale',
titleAfter: '.', titleAfter: '.',
subtitle: subtitle:
'Sviluppiamo soluzioni SaaS ad alta complessità per settori regolamentati.', 'Sviluppiamo soluzioni SaaS ad alta complessità per settori regolamentati.',
/** Paragrafi introduttivi */
paragraphs: [ paragraphs: [
"Ci occupiamo di progettazione di ecosistemi digitali evoluti. La nostra specializzazione risiede nell'integrazione di modelli di Intelligenza Artificiale all'interno di flussi di lavoro professionali dove precisione, sicurezza e affidabilità sono requisiti non negoziabili.", "Ci occupiamo di progettazione di ecosistemi digitali evoluti. La nostra specializzazione risiede nell'integrazione di modelli di Intelligenza Artificiale all'interno di flussi di lavoro professionali dove precisione, sicurezza e affidabilità sono requisiti non negoziabili.",
'Il nostro approccio si concentra sulla trasformazione di dati complessi in strumenti operativi fluidi, attraverso lo sviluppo di algoritmi proprietari, interfacce vocali e architetture cloud scalabili.', 'Il nostro approccio si concentra sulla trasformazione di dati complessi in strumenti operativi fluidi, attraverso lo sviluppo di algoritmi proprietari, interfacce vocali e architetture cloud scalabili.',
@ -26,4 +26,60 @@ export const hero = {
description: 'Sistemi avanzati per la gestione e lautomazione sanitaria.', description: 'Sistemi avanzati per la gestione e lautomazione sanitaria.',
}, },
] as const, ] as const,
},
en: {
badge: 'We give your time back',
titleBefore: "Software engineering for the era of ",
titleHighlight: 'Artificial Intelligence',
titleAfter: '.',
subtitle:
'We build high-complexity SaaS solutions for regulated industries.',
paragraphs: [
'We design advanced digital ecosystems. Our specialization is integrating AI models into professional workflows where accuracy, security, and reliability are non-negotiable requirements.',
'Our approach turns complex data into smooth operational tools through proprietary algorithms, voice interfaces, and scalable cloud-native architectures.',
] as const,
businessUnitsHeading: 'Our Business Units:',
businessUnitsIntro:
'We operate through specialized divisions to ensure deep domain expertise and vertical focus:',
businessUnits: [
{
name: 'Legal Tech',
description: 'Solutions for legal automation and regulatory analysis.',
},
{
name: 'Health Tech',
description:
'Advanced systems for healthcare workflow management and automation.',
},
] as const,
},
th: {
badge: 'คืนคุณค่าให้กับเวลาของคุณ',
titleBefore: 'วิศวกรรมซอฟต์แวร์สำหรับยุคของ',
titleHighlight: 'ปัญญาประดิษฐ์',
titleAfter: '.',
subtitle: 'เราพัฒนาโซลูชัน SaaS ที่ซับซ้อนสูงสำหรับอุตสาหกรรมที่มีข้อกำกับ',
paragraphs: [
'เราออกแบบระบบนิเวศดิจิทัลขั้นสูง โดยเชี่ยวชาญในการผสานโมเดล AI เข้ากับเวิร์กโฟลว์ระดับมืออาชีพที่ต้องการความแม่นยำ ความปลอดภัย และความน่าเชื่อถือ',
'แนวทางของเราคือเปลี่ยนข้อมูลที่ซับซ้อนให้เป็นเครื่องมือการทำงานที่ลื่นไหล ผ่านอัลกอริทึมเฉพาะทาง อินเทอร์เฟซเสียง และสถาปัตยกรรมคลาวด์ที่ขยายได้',
] as const,
businessUnitsHeading: 'หน่วยธุรกิจของเรา:',
businessUnitsIntro:
'เราดำเนินงานผ่านทีมเฉพาะทางเพื่อให้ได้ความเชี่ยวชาญเชิงลึกในแต่ละโดเมน:',
businessUnits: [
{
name: 'Legal Tech',
description: 'โซลูชันสำหรับงานกฎหมายอัตโนมัติและการวิเคราะห์ข้อกำกับ',
},
{
name: 'Health Tech',
description: 'ระบบขั้นสูงสำหรับการจัดการและทำงานอัตโนมัติในภาคสุขภาพ',
},
] as const,
},
} as const; } as const;
export const getHero = (locale: SupportedLocale) => heroByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const hero = heroByLocale.it;

View File

@ -3,13 +3,21 @@
* Modifica i file in questa cartella per aggiornare testi senza toccare il markup. * Modifica i file in questa cartella per aggiornare testi senza toccare il markup.
*/ */
export { siteMeta } from './site-meta'; export { siteMeta } from './site-meta';
export { getSiteMeta } from './site-meta';
export { chat } from './chat'; export { chat } from './chat';
export { navigation } from './navigation'; export { navigation } from './navigation';
export { getNavigation } from './navigation';
export { hero } from './hero'; export { hero } from './hero';
export { getHero } from './hero';
export { stats } from './stats'; export { stats } from './stats';
export { services } from './services'; export { services } from './services';
export { getServices } from './services';
export { stackSection } from './stack'; export { stackSection } from './stack';
export { getStackSection } from './stack';
export { productsSection } from './products'; export { productsSection } from './products';
export { faq } from './faq'; export { faq } from './faq';
export { getFaq } from './faq';
export { cta } from './cta'; export { cta } from './cta';
export { getCta } from './cta';
export { footer } from './footer'; export { footer } from './footer';
export { getFooter } from './footer';

View File

@ -1,7 +1,9 @@
import { chat } from './chat'; import { chat } from './chat';
/** Header — CTA unico: ingresso chat live (config in `chat.ts`) */ export type SupportedLocale = 'it' | 'en' | 'th';
export const navigation = {
const navigationByLocale = {
it: {
brandName: 'NexStudio', brandName: 'NexStudio',
items: [ items: [
{ label: 'Prodotti', href: '/#prodotti' }, { label: 'Prodotti', href: '/#prodotti' },
@ -16,4 +18,60 @@ export const navigation = {
ariaLabel: ariaLabel:
'Richiedi accesso alla chat NexStudio: stato operatore e chatbot, identificazione con email (link o codice) prima della sessione.', 'Richiedi accesso alla chat NexStudio: stato operatore e chatbot, identificazione con email (link o codice) prima della sessione.',
}, },
labels: {
mainNavAria: 'Principale',
mobileNavAria: 'Navigazione mobile',
languageSelectorAria: 'Seleziona lingua',
openMenuAria: 'Apri menu',
},
},
en: {
brandName: 'NexStudio',
items: [
{ label: 'Products', href: '/#prodotti' },
{ label: 'Services', href: '/#servizi' },
{ label: 'Stack', href: '/#stack' },
{ label: 'FAQ', href: '/#faq' },
{ label: 'Contact', href: '/#contatti' },
] as const,
cta: {
label: 'Lets chat',
linkTitle: 'Open NexStudio chat access request.',
ariaLabel:
'Request access to NexStudio chat: operator and chatbot availability, email verification link or code before session start.',
},
labels: {
mainNavAria: 'Main navigation',
mobileNavAria: 'Mobile navigation',
languageSelectorAria: 'Select language',
openMenuAria: 'Open menu',
},
},
th: {
brandName: 'NexStudio',
items: [
{ label: 'ผลิตภัณฑ์', href: '/#prodotti' },
{ label: 'บริการ', href: '/#servizi' },
{ label: 'สแตก', href: '/#stack' },
{ label: 'คำถามที่พบบ่อย', href: '/#faq' },
{ label: 'ติดต่อเรา', href: '/#contatti' },
] as const,
cta: {
label: 'เริ่มแชท',
linkTitle: 'เปิดคำขอเข้าใช้งานแชทของ NexStudio',
ariaLabel:
'ขอเข้าใช้งานแชท NexStudio: ตรวจสอบสถานะผู้ดูแลและแชทบอต พร้อมยืนยันตัวตนด้วยอีเมลก่อนเริ่มใช้งาน',
},
labels: {
mainNavAria: 'เมนูหลัก',
mobileNavAria: 'เมนูบนมือถือ',
languageSelectorAria: 'เลือกภาษา',
openMenuAria: 'เปิดเมนู',
},
},
} as const; } as const;
export const getNavigation = (locale: SupportedLocale) => navigationByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const navigation = navigationByLocale.it;

View File

@ -2,7 +2,10 @@
* Servizi (griglia bento). * Servizi (griglia bento).
* layout: wide = 2 colonne, tall = 2 righe + elenco puntato * layout: wide = 2 colonne, tall = 2 righe + elenco puntato
*/ */
export const services = { import type { SupportedLocale } from './navigation';
const servicesByLocale = {
it: {
eyebrow: 'I nostri servizi', eyebrow: 'I nostri servizi',
title: 'Implementazione, evoluzione e supporto', title: 'Implementazione, evoluzione e supporto',
lead: lead:
@ -10,7 +13,6 @@ export const services = {
cards: [ cards: [
{ {
layout: 'tall' as const, layout: 'tall' as const,
/** Sfera geodetica vettoriale (SVG) in basso a destra */
decoration: 'wireframe-globe' as const, decoration: 'wireframe-globe' as const,
title: 'Onboarding e formazione specialistica', title: 'Onboarding e formazione specialistica',
body: body:
@ -49,4 +51,104 @@ export const services = {
'Il mondo dellIA e delle normative (legali e sanitarie) è in costante mutamento. Il nostro servizio include laggiornamento continuo dei motori di analisi, garantendo che il software sia sempre allineato con le ultime tecnologie e le evoluzioni del mercato di riferimento.', 'Il mondo dellIA e delle normative (legali e sanitarie) è in costante mutamento. Il nostro servizio include laggiornamento continuo dei motori di analisi, garantendo che il software sia sempre allineato con le ultime tecnologie e le evoluzioni del mercato di riferimento.',
}, },
], ],
},
en: {
eyebrow: 'Our services',
title: 'Implementation, evolution, and support',
lead:
'At NexStudio, our commitment goes beyond software licensing. Our service model helps each professional and organization unlock the full value of AI integrated into LexAura and MediAura.',
cards: [
{
layout: 'tall' as const,
decoration: 'wireframe-globe' as const,
title: 'Onboarding and specialist training',
body:
'Adopting AI-powered systems is a methodological shift, not only a technical one. We provide dedicated programs for legal and medical teams to:',
bullets: [
'Improve interaction with search and summarization algorithms.',
'Master voice commands and document workflow automation.',
'Significantly reduce the technology learning curve.',
],
},
{
layout: 'wide' as const,
title: 'Technical support and operational consulting',
body:
'We provide continuous, high-level assistance to ensure operational continuity. NexStudio is not just a support vendor, but a strategic partner for legal firms and healthcare organizations.',
callout: {
title: '“Breaking Glass” intervention',
body:
'For critical urgency or systemic incidents, we offer rapid-access and priority intervention protocols. This emergency procedure bypasses standard support flows to restore continuity as fast as possible.',
},
},
{
layout: 'default' as const,
title: 'Custom extensions and integrations',
body:
'Although LexAura and MediAura are complete, ready-to-use products, complex organizations may still require specific interoperability needs.',
bullets: [
'API integration: we integrate our SaaS with your existing management systems.',
'Custom modules: we build tailored extensions inside our product perimeter to support unique workflows.',
],
},
{
layout: 'default' as const,
title: 'Evolutionary updates and maintenance',
body:
'AI and regulated frameworks evolve continuously. Our service includes ongoing analysis-engine updates to keep software aligned with new technologies and market requirements.',
},
],
},
th: {
eyebrow: 'บริการของเรา',
title: 'การติดตั้ง การพัฒนา และการสนับสนุน',
lead:
'ที่ NexStudio ความมุ่งมั่นของเราไม่ได้จบแค่การให้สิทธิ์ใช้งานซอฟต์แวร์ โมเดลบริการของเราช่วยให้ทีมงานดึงศักยภาพสูงสุดจาก AI ที่ผสานใน LexAura และ MediAura',
cards: [
{
layout: 'tall' as const,
decoration: 'wireframe-globe' as const,
title: 'การเริ่มใช้งานและการอบรมเฉพาะทาง',
body:
'การนำระบบ AI มาใช้ต้องอาศัยการเปลี่ยนผ่านเชิงวิธีการ ไม่ใช่แค่เทคนิค เรามีโปรแกรมอบรมสำหรับทีมกฎหมายและการแพทย์เพื่อ:',
bullets: [
'เพิ่มประสิทธิภาพการใช้งานอัลกอริทึมค้นหาและสรุปผล',
'ใช้งานคำสั่งเสียงและระบบงานเอกสารอัตโนมัติได้อย่างคล่องตัว',
'ลดช่วงเวลาเรียนรู้เทคโนโลยีอย่างมีนัยสำคัญ',
],
},
{
layout: 'wide' as const,
title: 'สนับสนุนทางเทคนิคและที่ปรึกษาการปฏิบัติงาน',
body:
'เราให้การสนับสนุนอย่างต่อเนื่องเพื่อคงความต่อเนื่องในการดำเนินงาน NexStudio ไม่ได้เป็นเพียงผู้ให้บริการซัพพอร์ต แต่เป็นพาร์ตเนอร์เชิงกลยุทธ์สำหรับองค์กร',
callout: {
title: 'การแทรกแซงแบบ “Breaking Glass”',
body:
'เมื่อเกิดเหตุเร่งด่วนหรือความผิดปกติระดับวิกฤต เรามีโปรโตคอลเข้าถึงด่วนและจัดลำดับความสำคัญสูงสุด เพื่อแก้ปัญหาได้ทันทีและรักษาความต่อเนื่องของบริการ',
},
},
{
layout: 'default' as const,
title: 'ส่วนขยายและการเชื่อมต่อแบบกำหนดเอง',
body:
'แม้ LexAura และ MediAura จะพร้อมใช้งานทันที แต่บางองค์กรอาจต้องการการเชื่อมต่อเฉพาะทางกับระบบเดิม',
bullets: [
'API integration: เชื่อม SaaS ของเราเข้ากับระบบจัดการที่ใช้อยู่แล้ว',
'Custom modules: ออกแบบส่วนขยายเฉพาะงานภายในขอบเขตผลิตภัณฑ์ของเรา',
],
},
{
layout: 'default' as const,
title: 'อัปเดตและบำรุงรักษาเชิงพัฒนา',
body:
'โลกของ AI และข้อกำกับเปลี่ยนแปลงตลอดเวลา บริการของเรารวมถึงการอัปเดตเอนจินวิเคราะห์อย่างต่อเนื่องเพื่อให้ซอฟต์แวร์ทันสมัยและสอดคล้องบริบทตลาด',
},
],
},
} as const; } as const;
export const getServices = (locale: SupportedLocale) => servicesByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const services = servicesByLocale.it;

View File

@ -1,6 +1,25 @@
import type { SupportedLocale } from './navigation';
/** Meta pagina principale — usato da BaseLayout */ /** Meta pagina principale — usato da BaseLayout */
export const siteMeta = { const siteMetaByLocale = {
it: {
title: 'NexStudio — Ingegneria del software e IA per settori regolamentati', title: 'NexStudio — Ingegneria del software e IA per settori regolamentati',
description: description:
'NexStudio: software company con sede a Bangkok. SaaS ad alta complessità, integrazione di IA in flussi professionali, Legal Tech e Health Tech.', 'NexStudio: software company con sede a Bangkok. SaaS ad alta complessità, integrazione di IA in flussi professionali, Legal Tech e Health Tech.',
},
en: {
title: 'NexStudio — Software engineering and AI for regulated industries',
description:
'NexStudio is a software company based in Bangkok. We build high-complexity SaaS products and integrate AI into professional workflows across Legal Tech and Health Tech.',
},
th: {
title: 'NexStudio — วิศวกรรมซอฟต์แวร์และ AI สำหรับอุตสาหกรรมที่มีข้อกำกับ',
description:
'NexStudio คือบริษัทซอฟต์แวร์ในกรุงเทพฯ พัฒนา SaaS ความซับซ้อนสูง และผสาน AI เข้ากับเวิร์กโฟลว์ระดับมืออาชีพในสาย Legal Tech และ Health Tech',
},
} as const; } as const;
export const getSiteMeta = (locale: SupportedLocale) => siteMetaByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const siteMeta = siteMetaByLocale.it;

View File

@ -1,9 +1,12 @@
import type { SupportedLocale } from './navigation';
/** Sezione stack — infrastruttura Cloudflare e sicurezza */ /** Sezione stack — infrastruttura Cloudflare e sicurezza */
export const stackSection = { const stackIt = {
eyebrow: 'Lo stack tecnologico', eyebrow: 'Lo stack tecnologico',
title: 'Infrastruttura e sicurezza', title: 'Infrastruttura e sicurezza',
lead: lead:
'In NexStudio, la qualità dellesperienza utente è inscindibile dalla solidità dellinfrastruttura. Per questo motivo ci affidiamo a partner globali ad alta efficienza che garantiscono i più elevati standard di disponibilità e protezione dei dati.', 'In NexStudio, la qualità dellesperienza utente è inscindibile dalla solidità dellinfrastruttura. Per questo motivo ci affidiamo a partner globali ad alta efficienza che garantiscono i più elevati standard di disponibilità e protezione dei dati.',
infrastructureHeading: 'Infrastruttura',
cloudflare: { cloudflare: {
title: 'Ecosistema Cloudflare', title: 'Ecosistema Cloudflare',
intro: intro:
@ -145,3 +148,168 @@ export const stackSection = {
] as const, ] as const,
}, },
} as const; } as const;
const stackByLocale = {
it: stackIt,
en: {
...stackIt,
eyebrow: 'Technology stack',
title: 'Infrastructure and security',
lead:
'At NexStudio, user experience quality is inseparable from infrastructure resilience. That is why we rely on high-efficiency global partners that provide strong availability and data protection standards.',
infrastructureHeading: 'Infrastructure',
cloudflare: {
...stackIt.cloudflare,
title: 'Cloudflare ecosystem',
intro:
'"Cloudflare helps accelerate performance, secure app availability, and optimize web experiences globally."',
points: [
{
title: 'Performance without compromise',
body:
'Access to our services remains low-latency worldwide, ensuring fluid usage even during complex data-processing operations.',
},
{
title: 'Advanced security',
body:
'We apply modern protection layers including end-to-end encryption, DDoS mitigation, and Web Application Firewall controls.',
},
{
title: 'Compliance and data integrity',
body:
'We use geographic data-control capabilities to align legal and medical data processing with local and international privacy requirements.',
},
] as const,
},
continuity: {
title: 'Continuity and scalability',
body:
'We provide professional-grade SLA uptime. Our cloud-native architecture scales instantly for workload peaks, large offices, and multi-site organizations.',
},
whyThisChoice: {
title: 'Why NexStudio',
points: [
'Regulated vertical focus: we design for Legal Tech and Health Tech, where traceability, privacy, and service continuity are core product requirements.',
'AI in real professional workflows: we embed models and automation with attention to accuracy, auditability, and human oversight.',
'End-to-end responsibility: architecture, development, and evolution stay under one ownership model for clearer priorities and delivery.',
] as const,
},
cloudflareBadge: {
...stackIt.cloudflareBadge,
label: 'Cloudflare ecosystem',
slogan: '"Helping build a better Internet."',
},
awsBadge: {
...stackIt.awsBadge,
label: 'AWS ecosystem',
slogan: '"The worlds most comprehensive and broadly adopted cloud platform."',
},
infrastructureCarousel: {
...stackIt.infrastructureCarousel,
ariaLabel: 'NexStudio infrastructure and technology partners',
items: [
{
...stackIt.infrastructureCarousel.items[0],
label: 'Cloudflare ecosystem',
slogan: '"Helping build a better Internet."',
},
{
...stackIt.infrastructureCarousel.items[1],
label: 'AWS ecosystem',
slogan: '"The worlds most comprehensive and broadly adopted cloud platform."',
},
{
...stackIt.infrastructureCarousel.items[2],
label: 'GitHub ecosystem',
},
{
...stackIt.infrastructureCarousel.items[3],
label: 'OpenAI ecosystem',
},
] as const,
},
},
th: {
...stackIt,
eyebrow: 'เทคโนโลยีสแตก',
title: 'โครงสร้างพื้นฐานและความปลอดภัย',
lead:
'ที่ NexStudio คุณภาพประสบการณ์ผู้ใช้แยกไม่ออกจากความมั่นคงของโครงสร้างพื้นฐาน เราจึงเลือกพาร์ตเนอร์ระดับโลกที่มีประสิทธิภาพสูงเพื่อให้ได้มาตรฐานด้านความพร้อมใช้งานและความปลอดภัยของข้อมูล',
infrastructureHeading: 'โครงสร้างพื้นฐาน',
cloudflare: {
...stackIt.cloudflare,
title: 'ระบบนิเวศ Cloudflare',
intro:
'"Cloudflare ช่วยเร่งประสิทธิภาพ รับประกันความพร้อมใช้งานของแอป และยกระดับประสบการณ์เว็บทั่วโลก"',
points: [
{
title: 'ประสิทธิภาพที่ไม่ประนีประนอม',
body:
'เข้าถึงบริการของเราได้อย่างหน่วงต่ำจากทุกที่ทั่วโลก แม้ในงานประมวลผลข้อมูลที่ซับซ้อน',
},
{
title: 'ความปลอดภัยขั้นสูง',
body:
'เราใช้กลไกป้องกันสมัยใหม่ เช่น การเข้ารหัส end-to-end การป้องกัน DDoS และ Web Application Firewall',
},
{
title: 'การปฏิบัติตามข้อกำกับและความถูกต้องของข้อมูล',
body:
'เราใช้ความสามารถในการกำกับตำแหน่งข้อมูลเพื่อให้การจัดการข้อมูลอ่อนไหวด้านกฎหมายและการแพทย์สอดคล้องกับข้อกำกับความเป็นส่วนตัว',
},
] as const,
},
continuity: {
title: 'ความต่อเนื่องและการขยายตัว',
body:
'เรารับประกัน uptime ระดับมืออาชีพตาม SLA และสถาปัตยกรรม cloud-native ที่ขยายได้ทันทีเมื่อมีโหลดสูง',
},
whyThisChoice: {
title: 'ทำไมต้อง NexStudio',
points: [
'เชี่ยวชาญอุตสาหกรรมที่มีข้อกำกับ: Legal Tech และ Health Tech',
'ผสาน AI เข้ากับงานจริง โดยคำนึงถึงความแม่นยำ การตรวจสอบย้อนหลัง และการกำกับโดยมนุษย์',
'รับผิดชอบแบบ end-to-end: สถาปัตยกรรม การพัฒนา และการต่อยอดอยู่ภายใต้ทีมเดียว',
] as const,
},
cloudflareBadge: {
...stackIt.cloudflareBadge,
label: 'ระบบนิเวศ Cloudflare',
slogan: '"ช่วยกันสร้างอินเทอร์เน็ตที่ดีกว่า"',
},
awsBadge: {
...stackIt.awsBadge,
label: 'ระบบนิเวศ AWS',
slogan: '"แพลตฟอร์มคลาวด์ที่ครอบคลุมและได้รับการใช้งานอย่างกว้างขวางที่สุดในโลก"',
},
infrastructureCarousel: {
...stackIt.infrastructureCarousel,
ariaLabel: 'พาร์ตเนอร์โครงสร้างพื้นฐานและเทคโนโลยีของ NexStudio',
items: [
{
...stackIt.infrastructureCarousel.items[0],
label: 'ระบบนิเวศ Cloudflare',
slogan: '"ช่วยกันสร้างอินเทอร์เน็ตที่ดีกว่า"',
},
{
...stackIt.infrastructureCarousel.items[1],
label: 'ระบบนิเวศ AWS',
slogan: '"แพลตฟอร์มคลาวด์ที่ครอบคลุมและได้รับการใช้งานอย่างกว้างขวางที่สุดในโลก"',
},
{
...stackIt.infrastructureCarousel.items[2],
label: 'ระบบนิเวศ GitHub',
},
{
...stackIt.infrastructureCarousel.items[3],
label: 'ระบบนิเวศ OpenAI',
},
] as const,
},
},
} as const;
export const getStackSection = (locale: SupportedLocale) => stackByLocale[locale];
/** Retro-compatibilità: fallback italiano. */
export const stackSection = stackByLocale.it;

View File

@ -1,6 +1,7 @@
--- ---
import BaseLayout from './BaseLayout.astro'; import BaseLayout from './BaseLayout.astro';
import SiteFooter from '../components/SiteFooter.astro'; import SiteFooter from '../components/SiteFooter.astro';
import { getNavigation } from '../data/home';
interface Props { interface Props {
title: string; title: string;
@ -29,7 +30,8 @@ const printDate = new Intl.DateTimeFormat('it-IT', {
const printSource = Astro.url.href; const printSource = Astro.url.href;
const path = Astro.url.pathname; const path = Astro.url.pathname;
const localeMatch = path.match(/^\/(en|th)(\/|$)/); const localeMatch = path.match(/^\/(en|th)(\/|$)/);
const currentLocale = localeMatch?.[1] ?? 'it'; const currentLocale = (localeMatch?.[1] ?? 'it') as 'it' | 'en' | 'th';
const navigation = getNavigation(currentLocale);
const strippedPath = path.replace(/^\/(en|th)(?=\/|$)/, '') || '/'; const strippedPath = path.replace(/^\/(en|th)(?=\/|$)/, '') || '/';
const localeHref = (locale: 'it' | 'en' | 'th') => { const localeHref = (locale: 'it' | 'en' | 'th') => {
if (locale === 'it') return strippedPath; if (locale === 'it') return strippedPath;
@ -71,7 +73,7 @@ const langMeta = {
<details class="relative" data-lang-dropdown> <details class="relative" data-lang-dropdown>
<summary <summary
class="inline-flex h-10 cursor-pointer list-none items-center gap-2 rounded-lg border border-nx-border/80 bg-nx-bg/50 px-3 text-xs font-medium text-nx-fg transition-colors hover:border-sky-500/35 hover:text-white" class="inline-flex h-10 cursor-pointer list-none items-center gap-2 rounded-lg border border-nx-border/80 bg-nx-bg/50 px-3 text-xs font-medium text-nx-fg transition-colors hover:border-sky-500/35 hover:text-white"
aria-label="Seleziona lingua" aria-label={navigation.labels.languageSelectorAria}
> >
<img src={langMeta[currentLocale].flagSrc} alt="" class="h-3.5 w-5 rounded-[2px] object-cover" loading="lazy" decoding="async" /> <img src={langMeta[currentLocale].flagSrc} alt="" class="h-3.5 w-5 rounded-[2px] object-cover" loading="lazy" decoding="async" />
<span>{langMeta[currentLocale].code}</span> <span>{langMeta[currentLocale].code}</span>
@ -129,7 +131,7 @@ const langMeta = {
</details> </details>
<a <a
class="text-sm font-medium text-sky-400/90 hover:text-sky-300" class="text-sm font-medium text-sky-400/90 hover:text-sky-300"
href={localize('/#contatti')}>Contattaci</a href={localize('/#contatti')}>{navigation.items[4]?.label ?? 'Contattaci'}</a
> >
</div> </div>
</div> </div>

View File

@ -82,12 +82,9 @@ async function sendWithResend(apiKey: string, fromEmail: string, toEmail: string
function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string { function resolveDepartmentRecipient(runtimeEnv: RuntimeEnv, defaultRecipient: string, department: string): string {
const normalized = department.trim().toLowerCase(); const normalized = department.trim().toLowerCase();
const map: Record<string, string> = { const map: Record<string, string> = {
support: readEnv('CONTACT_FORM_TO_EMAIL_SUPPORT', runtimeEnv), info: readEnv('CONTACT_FORM_TO_EMAIL_INFO', runtimeEnv) || defaultRecipient,
privacy: readEnv('CONTACT_FORM_TO_EMAIL_PRIVACY', runtimeEnv), privacy: readEnv('CONTACT_FORM_TO_EMAIL_PRIVACY', runtimeEnv),
security: readEnv('CONTACT_FORM_TO_EMAIL_SECURITY', runtimeEnv), careers: readEnv('CONTACT_FORM_TO_EMAIL_CAREERS', runtimeEnv),
ethics: readEnv('CONTACT_FORM_TO_EMAIL_ETHICS', runtimeEnv),
legal: readEnv('CONTACT_FORM_TO_EMAIL_LEGAL', runtimeEnv),
hr: readEnv('CONTACT_FORM_TO_EMAIL_HR', runtimeEnv),
}; };
return map[normalized] || defaultRecipient; return map[normalized] || defaultRecipient;
} }
@ -111,7 +108,7 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
const company = required(form.get('company')); const company = required(form.get('company'));
const email = required(form.get('email')); const email = required(form.get('email'));
const country = required(form.get('country')); const country = required(form.get('country'));
const department = required(form.get('department')) || 'general'; const department = required(form.get('department')) || 'info';
const message = required(form.get('message')); const message = required(form.get('message'));
if (!name || !email || !country || !message) { if (!name || !email || !country || !message) {

View File

@ -6,21 +6,33 @@ import ContactFormInline from '../components/ContactFormInline.astro';
import CardBottomRightTexture from '../components/CardBottomRightTexture.astro'; import CardBottomRightTexture from '../components/CardBottomRightTexture.astro';
import SiteFooter from '../components/SiteFooter.astro'; import SiteFooter from '../components/SiteFooter.astro';
import { import {
siteMeta, getSiteMeta,
navigation, getNavigation,
hero, getHero,
stats, stats,
services, getServices,
stackSection, getStackSection,
productsSection, productsSection,
faq, getFaq,
cta, getCta,
} from '../data/home'; } from '../data/home';
const infrastructureCarouselItems = stackSection.infrastructureCarousel.items;
const path = Astro.url.pathname; const path = Astro.url.pathname;
const localeMatch = path.match(/^\/(en|th)(\/|$)/); const localeMatch = path.match(/^\/(en|th)(\/|$)/);
const currentLocale = localeMatch?.[1] ?? 'it'; const currentLocale = (localeMatch?.[1] ?? 'it') as 'it' | 'en' | 'th';
const navigation = getNavigation(currentLocale);
const siteMeta = getSiteMeta(currentLocale);
const hero = getHero(currentLocale);
const services = getServices(currentLocale);
const stackSection = getStackSection(currentLocale);
const faq = getFaq(currentLocale);
const cta = getCta(currentLocale);
const infrastructureCarouselItems = stackSection.infrastructureCarousel.items;
const pageLabels = {
it: { focus: 'Focus' },
en: { focus: 'Focus' },
th: { focus: 'จุดเน้น' },
}[currentLocale];
const localize = (href: string) => { const localize = (href: string) => {
if (currentLocale === 'it') return href; if (currentLocale === 'it') return href;
if (href.startsWith(`/${currentLocale}`)) return href; if (href.startsWith(`/${currentLocale}`)) return href;
@ -64,7 +76,7 @@ const langMeta = {
<nav <nav
class="hidden items-center gap-8 text-sm md:flex" class="hidden items-center gap-8 text-sm md:flex"
aria-label="Principale" aria-label={navigation.labels.mainNavAria}
> >
{navigation.items.map((item) => { {navigation.items.map((item) => {
const sectionId = item.href.includes('#') const sectionId = item.href.includes('#')
@ -87,7 +99,7 @@ const langMeta = {
<details class="relative hidden sm:block" data-lang-dropdown> <details class="relative hidden sm:block" data-lang-dropdown>
<summary <summary
class="inline-flex h-10 cursor-pointer list-none items-center gap-2 rounded-lg border border-nx-border/80 bg-nx-bg/50 px-3 text-xs font-medium text-nx-fg transition-colors hover:border-sky-500/35 hover:text-white" class="inline-flex h-10 cursor-pointer list-none items-center gap-2 rounded-lg border border-nx-border/80 bg-nx-bg/50 px-3 text-xs font-medium text-nx-fg transition-colors hover:border-sky-500/35 hover:text-white"
aria-label="Seleziona lingua" aria-label={navigation.labels.languageSelectorAria}
> >
<img src={langMeta[currentLocale].flagSrc} alt="" class="h-3.5 w-5 rounded-[2px] object-cover" loading="lazy" decoding="async" /> <img src={langMeta[currentLocale].flagSrc} alt="" class="h-3.5 w-5 rounded-[2px] object-cover" loading="lazy" decoding="async" />
<span>{langMeta[currentLocale].code}</span> <span>{langMeta[currentLocale].code}</span>
@ -157,7 +169,7 @@ const langMeta = {
@click="mobile = !mobile" @click="mobile = !mobile"
:aria-expanded="mobile" :aria-expanded="mobile"
aria-controls="mobile-nav" aria-controls="mobile-nav"
aria-label="Apri menu" aria-label={navigation.labels.openMenuAria}
> >
<svg <svg
x-show="!mobile" x-show="!mobile"
@ -191,7 +203,7 @@ const langMeta = {
x-cloak x-cloak
class="border-b border-nx-border bg-black px-4 py-4 md:hidden" class="border-b border-nx-border bg-black px-4 py-4 md:hidden"
> >
<nav class="flex flex-col gap-3 text-sm" aria-label="Mobile"> <nav class="flex flex-col gap-3 text-sm" aria-label={navigation.labels.mobileNavAria}>
{navigation.items.map((item) => { {navigation.items.map((item) => {
const sectionId = item.href.includes('#') const sectionId = item.href.includes('#')
? item.href.split('#').pop()! ? item.href.split('#').pop()!
@ -356,7 +368,7 @@ const langMeta = {
pi === 0 ? 'text-sky-200/85' : 'text-emerald-200/85', pi === 0 ? 'text-sky-200/85' : 'text-emerald-200/85',
]} ]}
> >
<span class="text-nx-muted font-normal">Focus — </span> <span class="text-nx-muted font-normal">{pageLabels.focus} — </span>
{product.focus} {product.focus}
</p> </p>
<p class="mt-5 text-sm leading-relaxed text-nx-muted sm:text-base"> <p class="mt-5 text-sm leading-relaxed text-nx-muted sm:text-base">
@ -497,7 +509,7 @@ const langMeta = {
class="relative z-[1] mt-12 rounded-2xl border border-orange-500/20 bg-gradient-to-br from-orange-950/20 to-nx-elevated/40 p-6 sm:p-8" class="relative z-[1] mt-12 rounded-2xl border border-orange-500/20 bg-gradient-to-br from-orange-950/20 to-nx-elevated/40 p-6 sm:p-8"
> >
<p class="text-xs font-medium uppercase tracking-wider text-orange-200/90"> <p class="text-xs font-medium uppercase tracking-wider text-orange-200/90">
Infrastruttura {stackSection.infrastructureHeading}
</p> </p>
<div <div
class="nx-infra-carousel mt-5" class="nx-infra-carousel mt-5"

View File

@ -6,13 +6,32 @@ import SubpageLayout from '../layouts/SubpageLayout.astro';
title="Novità — NexStudio" title="Novità — NexStudio"
description="Novità, release e articoli da NexStudio." description="Novità, release e articoli da NexStudio."
heading="Novità" heading="Novità"
lead="Hub comunicazioni: collegate qui il vostro blog CMS, oppure elencate manualmente le notizie." lead="Aggiornamenti ufficiali su piattaforma, compliance, infrastruttura e contenuti editoriali."
> >
<p> <p class="rounded-xl border border-emerald-500/30 bg-emerald-500/10 px-4 py-3 text-emerald-100">
Quando avrete contenuti da pubblicare (release di prodotto, note di Stato editoriale: canale attivo. Le prossime pubblicazioni verranno aggiornate su questa pagina con frequenza periodica.
conformità, eventi), potete sostituire questa pagina con una raccolta
articoli Astro, un redirect verso Medium/Substack, o un headless CMS.
</p> </p>
<h2>Programma editoriale</h2>
<ul>
<li><strong>Release tecniche:</strong> aggiornamenti su funzionalita, integrazioni e miglioramenti di stabilita.</li>
<li><strong>Compliance:</strong> novita su policy, sicurezza applicativa e adempimenti normativi.</li>
<li><strong>Case e risultati:</strong> note operative su adozione, scalabilita e impatti di business.</li>
</ul>
<h2>Canali di distribuzione</h2>
<ul>
<li>Questa pagina resta il punto di riferimento ufficiale per annunci e changelog sintetici.</li>
<li>La newsletter verra attivata appena disponibile il canale comunicazioni dedicato.</li>
<li>I contenuti long-form potranno essere pubblicati anche su canali esterni, con rimando qui.</li>
</ul>
<h2>Archivio</h2> <h2>Archivio</h2>
<p class="text-nx-muted">Nessuna news pubblicata per il momento.</p> <p class="text-nx-muted">
In questa fase il sito e in pre-lancio operativo. Il primo blocco di news verra pubblicato con il go-live ufficiale.
</p>
<p>
Per richieste stampa, partnership o approfondimenti tecnici e possibile usare il form nella sezione
<a href="/#contatti" class="text-sky-300 hover:text-sky-200">Contatti</a>.
</p>
</SubpageLayout> </SubpageLayout>

View File

@ -11,6 +11,15 @@ interface ContactFormCfg {
fieldDepartment: string; fieldDepartment: string;
fieldEmail: string; fieldEmail: string;
fieldMessage: string; fieldMessage: string;
turnstileError: string;
configError: string;
mailtoFallbackSubject: string;
mailtoLabelName: string;
mailtoLabelCompany: string;
mailtoLabelEmail: string;
mailtoLabelCountry: string;
mailtoLabelDepartment: string;
mailtoDefaultDepartment: string;
} }
function readCfg(): ContactFormCfg { function readCfg(): ContactFormCfg {
@ -29,6 +38,15 @@ function readCfg(): ContactFormCfg {
fieldDepartment: 'department', fieldDepartment: 'department',
fieldEmail: 'email', fieldEmail: 'email',
fieldMessage: 'message', fieldMessage: 'message',
turnstileError: 'Complete anti-spam verification before sending.',
configError: 'Contact form endpoint is not configured.',
mailtoFallbackSubject: 'Website contact',
mailtoLabelName: 'Name',
mailtoLabelCompany: 'Company',
mailtoLabelEmail: 'Email',
mailtoLabelCountry: 'Country',
mailtoLabelDepartment: 'Department',
mailtoDefaultDepartment: 'general',
}; };
} }
return JSON.parse(raw) as ContactFormCfg; return JSON.parse(raw) as ContactFormCfg;
@ -52,8 +70,7 @@ function bind(): void {
if (!token) { if (!token) {
e.preventDefault(); e.preventDefault();
if (err) { if (err) {
err.textContent = err.textContent = cfg.turnstileError;
'Completa il controllo anti-spam prima di inviare il messaggio.';
err.classList.remove('hidden'); err.classList.remove('hidden');
} }
return; return;
@ -72,17 +89,20 @@ function bind(): void {
const department = String(fd.get(cfg.fieldDepartment) ?? '').trim(); const department = String(fd.get(cfg.fieldDepartment) ?? '').trim();
const message = String(fd.get(cfg.fieldMessage) ?? '').trim(); const message = String(fd.get(cfg.fieldMessage) ?? '').trim();
if (cfg.mailto) { if (cfg.mailto) {
const subject = encodeURIComponent(cfg.notifySubject || 'Contatto sito'); const subject = encodeURIComponent(
const companyLine = company ? `\nAzienda: ${company}` : ''; cfg.notifySubject || cfg.mailtoFallbackSubject || 'Website contact',
);
const companyLine = company
? `\n${cfg.mailtoLabelCompany}: ${company}`
: '';
const body = encodeURIComponent( const body = encodeURIComponent(
`Nome: ${name}${companyLine}\nEmail: ${email}\nPaese: ${country}\nReparto: ${department || 'general'}\n\n${message}`, `${cfg.mailtoLabelName}: ${name}${companyLine}\n${cfg.mailtoLabelEmail}: ${email}\n${cfg.mailtoLabelCountry}: ${country}\n${cfg.mailtoLabelDepartment}: ${department || cfg.mailtoDefaultDepartment}\n\n${message}`,
); );
window.location.href = `mailto:${cfg.mailto}?subject=${subject}&body=${body}`; window.location.href = `mailto:${cfg.mailto}?subject=${subject}&body=${body}`;
return; return;
} }
if (err) { if (err) {
err.textContent = err.textContent = cfg.configError;
'Configura `contactForm.actionUrl` (Formspree) o `contactForm.mailto` in `src/data/home/cta.ts`.';
err.classList.remove('hidden'); err.classList.remove('hidden');
} }
}); });