diff --git a/astro.config.mjs b/astro.config.mjs index 0daaf65..4d5273f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -3,10 +3,12 @@ import { defineConfig } from 'astro/config'; import tailwindcss from "@tailwindcss/vite"; import react from '@astrojs/react'; +import sitemap from '@astrojs/sitemap'; + export default defineConfig({ site: 'https://sell.markket.place', - integrations: [react()], + integrations: [react(), sitemap()], vite: { plugins: [tailwindcss()], }, -}); +}); \ No newline at end of file diff --git a/markket.config.ts b/markket.config.ts new file mode 100644 index 0000000..951d109 --- /dev/null +++ b/markket.config.ts @@ -0,0 +1,7 @@ + +export const markket = { + store_slug: import.meta.env.PUBLIC_STORE_SLUG, + api_url: import.meta.env.PUBLIC_STRAPI_URL, + sync_interval: 6000, +}; + diff --git a/package-lock.json b/package-lock.json index 222b04a..2ff4312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@astrojs/react": "^4.4.0", + "@astrojs/sitemap": "^3.6.0", "@tabler/icons-react": "^3.35.0", "@tailwindcss/vite": "^4.1.13", "astro": "^5.13.9", @@ -87,6 +88,16 @@ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@astrojs/sitemap": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.6.0.tgz", + "integrity": "sha512-4aHkvcOZBWJigRmMIAJwRQXBS+ayoP5z40OklTXYXhUDhwusz+DyDl+nSshY6y9DvkVEavwNcFO8FD81iGhXjg==", + "dependencies": { + "sitemap": "^8.0.0", + "stream-replace-string": "^2.0.0", + "zod": "^3.25.76" + } + }, "node_modules/@astrojs/telemetry": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", @@ -1982,6 +1993,14 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2112,6 +2131,11 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4853,6 +4877,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -4931,6 +4960,29 @@ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" }, + "node_modules/sitemap": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.0.tgz", + "integrity": "sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + }, "node_modules/smol-toml": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.4.2.tgz", @@ -4959,6 +5011,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stream-replace-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", + "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==" + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", diff --git a/package.json b/package.json index 7eb0d50..115aaa5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@astrojs/react": "^4.4.0", + "@astrojs/sitemap": "^3.6.0", "@tabler/icons-react": "^3.35.0", "@tailwindcss/vite": "^4.1.13", "astro": "^5.13.9", diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..7ff9694 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / + +Sitemap: https://sell.markket.place/sitemap-index.xml + diff --git a/src/components/subscribe-form.tsx b/src/components/subscribe-form.tsx new file mode 100644 index 0000000..145abf9 --- /dev/null +++ b/src/components/subscribe-form.tsx @@ -0,0 +1,175 @@ + + +import React, { useState, useRef, useEffect, type FormEvent } from 'react'; +import { IconRefreshAlert, IconMailbox, IconSquareRoundedX, IconCheck } from '@tabler/icons-react'; +import { markket } from '../../markket.config'; + +export interface SubscribeFormProps { + store: { + documentId: string; + }; +} + +export function SubscribeForm({ store }: SubscribeFormProps) { + const [email, setEmail] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + const successRef = useRef(null); + + useEffect(() => { + if (isSuccess && successRef.current) successRef.current.focus(); + }, [isSuccess]); + + useEffect(() => { + if (!isSuccess) return; + const t = setTimeout(() => setIsSuccess(false), 6000); + return () => clearTimeout(t); + }, [isSuccess]); + + const validateEmail = (value: string) => /^\S+@\S+\.\S+$/.test(value); + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + setError(''); + setIsSubmitting(true); + + if (!validateEmail(email)) { + setError('Please enter a valid email address'); + setIsSubmitting(false); + return; + } + + try { + const response = await fetch(new URL(`/api/subscribers`, markket.api_url), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + data: { + Email: email, + stores: store?.documentId ? [store.documentId] : [], + }, + }), + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data?.message || 'Subscription failed'); + + setIsSuccess(true); + setEmail(''); + } catch (err) { + console.error('Subscription error:', err); + setError(err instanceof Error ? err.message : 'Failed to subscribe. Please try again.'); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
+
+
+ +
+
+

Join our newsletter

+

Short, delightful updates about products, design notes, and occasional exclusive offers. No spam — ever.

+ +
    +
  • + + Curated product updates and releases +
  • +
  • + + Design insights and short articles +
  • +
+
+ +
+
+ {error && ( + + )} + + +
+
+ + setEmail(e.target.value)} + placeholder="you@yourdomain.com" + required + disabled={isSubmitting} + autoComplete="email" + aria-describedby={error ? 'subscribe-error' : 'subscribe-success'} + className={`w-full pl-11 pr-4 py-4 rounded-2xl border transition-shadow text-base text-gray-900 placeholder-gray-400 bg-white focus:outline-none focus:ring-4 focus:ring-indigo-100 disabled:opacity-60 ${error ? 'border-red-300' : 'border-gray-200'}`} + style={{ minWidth: '20rem' }} + /> +
+ +
+ +
+
+ +

We respect your privacy. Unsubscribe anytime.

+ + {isSuccess && ( +
+
+
+ +
+

You're subscribed — thank you!

+

We'll send occasional updates to your inbox.

+
+ +
+
+
+ )} +
+
+
+
+
+ ); +} + +export default SubscribeForm; diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index a82ff27..f58a2c2 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,6 +1,6 @@ --- import "../styles/base.css"; -import Posthog from '../components/posthog.astro'; +import Posthog from "../components/posthog.astro"; export interface Props { title?: string; @@ -8,7 +8,7 @@ export interface Props { image?: string; canonical?: string; noindex?: boolean; - type?: 'website' | 'article' | 'product'; + type?: "website" | "article" | "product"; publishedTime?: string; modifiedTime?: string; author?: string; @@ -26,7 +26,7 @@ const { image = "/favicon.svg", canonical, noindex = false, - type = 'website', + type = "website", publishedTime, modifiedTime, author, @@ -35,29 +35,30 @@ const { alternateLocales = [], structuredData, className = "", - favicon = '/favicon.png' + favicon = "/favicon.png", } = Astro.props; // Get the canonical URL - use provided canonical or current URL const canonicalURL = canonical || new URL(Astro.url.pathname, Astro.site); // Ensure image is absolute URL -const absoluteImage = image?.startsWith('http') +const absoluteImage = image?.startsWith("http") ? image : new URL(image, Astro.site); // Generate structured data for SEO const defaultStructuredData = { "@context": "https://schema.org", - "@type": type === 'article' ? "Article" : type === 'product' ? "Product" : "WebSite", - "name": title, - "description": description, - "url": canonicalURL, - ...(image && { "image": absoluteImage }), - ...(author && { "author": { "@type": "Person", "name": author } }), - ...(publishedTime && { "datePublished": publishedTime }), - ...(modifiedTime && { "dateModified": modifiedTime }), - ...(siteName && { "publisher": { "@type": "Organization", "name": siteName } }) + "@type": + type === "article" ? "Article" : type === "product" ? "Product" : "WebSite", + name: title, + description: description, + url: canonicalURL, + ...(image && { image: absoluteImage }), + ...(author && { author: { "@type": "Person", name: author } }), + ...(publishedTime && { datePublished: publishedTime }), + ...(modifiedTime && { dateModified: modifiedTime }), + ...(siteName && { publisher: { "@type": "Organization", name: siteName } }), }; const finalStructuredData = structuredData || defaultStructuredData; @@ -70,6 +71,7 @@ const finalStructuredData = structuredData || defaultStructuredData; + {title} @@ -86,11 +88,21 @@ const finalStructuredData = structuredData || defaultStructuredData; - {alternateLocales.map(({ locale: altLocale, url }) => ( - - ))} - {publishedTime && } - {modifiedTime && } + { + alternateLocales.map(({ locale: altLocale, url }) => ( + + )) + } + { + publishedTime && ( + + ) + } + { + modifiedTime && ( + + ) + } {author && } @@ -113,12 +125,17 @@ const finalStructuredData = structuredData || defaultStructuredData; /> - {alternateLocales.map(({ locale: altLocale, url }) => ( - - ))} + { + alternateLocales.map(({ locale: altLocale, url }) => ( + + )) + } -