SEO: FAQ schema, per-product OG cards, stable sitemap dates
- Add FAQPage structured data + visible FAQ sections to Wownerogue, Webshops, Neroswap, and Services (covers high-intent 'provably fair', 'no-KYC', and 'revenue share' style queries; visible Q&A matches the schema). - Generate branded 1200x630 OG cards per product and wire them into each product page's openGraph + twitter (summary_large_image), replacing the square app icons that previously got cropped in social previews. - Sitemap: stop stamping every static page with new Date() on each build; use a single content-update date so freshness signals are not diluted.
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 42 KiB |
@@ -15,7 +15,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Bauhaus Echo: Visual Memory Puzzle Game | Such Software",
|
title: "Bauhaus Echo: Visual Memory Puzzle Game | Such Software",
|
||||||
description: "A Bauhaus-inspired memory puzzle game with 5 modes and a pressure-free Zen sandbox. Play free on iOS, Android, web, and desktop.",
|
description: "A Bauhaus-inspired memory puzzle game with 5 modes and a pressure-free Zen sandbox. Play free on iOS, Android, web, and desktop.",
|
||||||
type: "website",
|
type: "website",
|
||||||
images: [{ url: "/images/products/bauhaus-echo.png", width: 512, height: 512, alt: "Bauhaus Echo" }],
|
images: ["/images/og/bauhaus-echo.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/bauhaus-echo.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Custom Web Apps & Real-Time Platforms | Such Software",
|
title: "Custom Web Apps & Real-Time Platforms | Such Software",
|
||||||
description: "Fast, accessible real-time web apps: multiplayer games, live dashboards, and crypto platforms with WebSocket sync. Next.js, Socket.io, WebRTC.",
|
description: "Fast, accessible real-time web apps: multiplayer games, live dashboards, and crypto platforms with WebSocket sync. Next.js, Socket.io, WebRTC.",
|
||||||
type: "website",
|
type: "website",
|
||||||
|
images: ["/images/og/custom-websites.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/custom-websites.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MobileNav } from "@/components/layout/mobile-nav";
|
|||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { JsonLd, breadcrumbLd, softwareAppLd } from "@/components/seo/json-ld";
|
import { JsonLd, breadcrumbLd, softwareAppLd, faqLd } from "@/components/seo/json-ld";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -17,6 +17,11 @@ export const metadata: Metadata = {
|
|||||||
"Compare live Monero, Wownero, and Bitcoin prices across 6 DEX and no-KYC CEX orderbooks in real time. No accounts, no custody, no tracking.",
|
"Compare live Monero, Wownero, and Bitcoin prices across 6 DEX and no-KYC CEX orderbooks in real time. No accounts, no custody, no tracking.",
|
||||||
type: "website",
|
type: "website",
|
||||||
url: "https://such.software/products/neroswap",
|
url: "https://such.software/products/neroswap",
|
||||||
|
images: ["/images/og/neroswap.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/neroswap.png"],
|
||||||
},
|
},
|
||||||
keywords: [
|
keywords: [
|
||||||
"neroswap",
|
"neroswap",
|
||||||
@@ -33,6 +38,13 @@ export const metadata: Metadata = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{ q: "Is Neroswap KYC-free?", a: "Yes. Neroswap aggregates public orderbook data and never asks for an account or identity. Every exchange it lists is either fully decentralized or no-KYC." },
|
||||||
|
{ q: "Does Neroswap hold my coins?", a: "No. Neroswap is a read-only price and orderbook aggregator, not an exchange or custodian. You trade on whichever source venue you choose; Neroswap never touches funds." },
|
||||||
|
{ q: "What does Neroswap actually do?", a: "It pulls live prices from six DEX and no-KYC CEX venues and compares Monero, Wownero, and Bitcoin markets in real time, then links each row straight to the source market so you can trade at the best price." },
|
||||||
|
{ q: "Which coins are supported?", a: "Monero (XMR), Wownero (WOW), and Bitcoin (BTC) are the headline markets, alongside other pairs offered by the supported exchanges." },
|
||||||
|
];
|
||||||
|
|
||||||
const exchanges = [
|
const exchanges = [
|
||||||
{
|
{
|
||||||
name: "BasicSwap",
|
name: "BasicSwap",
|
||||||
@@ -110,6 +122,7 @@ export default function NeroswapPage() {
|
|||||||
price: "0",
|
price: "0",
|
||||||
offerUrl: "https://neroswap.com",
|
offerUrl: "https://neroswap.com",
|
||||||
})} />
|
})} />
|
||||||
|
<JsonLd data={faqLd(faqs)} />
|
||||||
<Header />
|
<Header />
|
||||||
<div
|
<div
|
||||||
id="main-content"
|
id="main-content"
|
||||||
@@ -230,7 +243,17 @@ export default function NeroswapPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mt-12">
|
<h2 className="text-2xl font-bold mt-12 mb-6">FAQ</h2>
|
||||||
|
<div className="space-y-6 mb-12">
|
||||||
|
{faqs.map((f) => (
|
||||||
|
<div key={f.q}>
|
||||||
|
<h3 className="font-bold mb-1 text-yellow-600 dark:text-yellow-400">{f.q}</h3>
|
||||||
|
<p className="text-muted-foreground">{f.a}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
Related:{" "}
|
Related:{" "}
|
||||||
<Link href="/products/smirk-wallet" className="text-yellow-600 dark:text-yellow-400 underline">Smirk Wallet</Link>,{" "}
|
<Link href="/products/smirk-wallet" className="text-yellow-600 dark:text-yellow-400 underline">Smirk Wallet</Link>,{" "}
|
||||||
<Link href="/products/wownerogue" className="text-yellow-600 dark:text-yellow-400 underline">Wownerogue</Link>, and our{" "}
|
<Link href="/products/wownerogue" className="text-yellow-600 dark:text-yellow-400 underline">Wownerogue</Link>, and our{" "}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Smirk Wallet: Non-Custodial Crypto Tipping by Username",
|
title: "Smirk Wallet: Non-Custodial Crypto Tipping by Username",
|
||||||
description: "Tip BTC, LTC, XMR, WOW, and GRIN on Telegram or Discord by username. A non-custodial browser wallet: your keys never leave your device.",
|
description: "Tip BTC, LTC, XMR, WOW, and GRIN on Telegram or Discord by username. A non-custodial browser wallet: your keys never leave your device.",
|
||||||
type: "website",
|
type: "website",
|
||||||
|
images: ["/images/og/smirk-wallet.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/smirk-wallet.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Such Moon Launch: Pixel Rocket Arcade Game | Such Software",
|
title: "Such Moon Launch: Pixel Rocket Arcade Game | Such Software",
|
||||||
description: "Fly a rocket across the solar system in this Wownero-themed pixel arcade. 11 levels, 4 weapons, tilt-to-steer, endless mode. Free on iOS, Android, web, desktop.",
|
description: "Fly a rocket across the solar system in this Wownero-themed pixel arcade. 11 levels, 4 weapons, tilt-to-steer, endless mode. Free on iOS, Android, web, desktop.",
|
||||||
type: "website",
|
type: "website",
|
||||||
|
images: ["/images/og/such-moon-launch.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/such-moon-launch.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Suchoice: Spin the Wheel & AI Decision Maker App | Such Software",
|
title: "Suchoice: Spin the Wheel & AI Decision Maker App | Such Software",
|
||||||
description: "Can't decide? Snap a photo and let AI read your options, spin the wheel, flip a coin, or roll 3D dice. The fun decision-maker app for iOS and Android.",
|
description: "Can't decide? Snap a photo and let AI read your options, spin the wheel, flip a coin, or roll 3D dice. The fun decision-maker app for iOS and Android.",
|
||||||
type: "website",
|
type: "website",
|
||||||
images: [{ url: "/images/products/suchoice.png", width: 512, height: 512, alt: "Suchoice" }],
|
images: ["/images/og/suchoice.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/suchoice.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ export const metadata: Metadata = {
|
|||||||
title: "Vegan IQ: Plant-Based Trivia Game | Such Software",
|
title: "Vegan IQ: Plant-Based Trivia Game | Such Software",
|
||||||
description: "Test your plant-based knowledge with 1,000+ trivia questions across 8 categories, plus a daily challenge and weekly quiz. Free vegan trivia for iOS and Android.",
|
description: "Test your plant-based knowledge with 1,000+ trivia questions across 8 categories, plus a daily challenge and weekly quiz. Free vegan trivia for iOS and Android.",
|
||||||
type: "website",
|
type: "website",
|
||||||
images: [{ url: "/images/products/vegan-iq.png", width: 512, height: 512, alt: "Vegan IQ" }],
|
images: ["/images/og/vegan-iq.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/vegan-iq.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Header } from "@/components/layout/header";
|
|||||||
import { MobileNav } from "@/components/layout/mobile-nav";
|
import { MobileNav } from "@/components/layout/mobile-nav";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
import { Button } from "@repo/ui/components/button";
|
import { Button } from "@repo/ui/components/button";
|
||||||
import { JsonLd, breadcrumbLd } from "@/components/seo/json-ld";
|
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
@@ -14,9 +14,22 @@ export const metadata: Metadata = {
|
|||||||
title: "Custom Online Stores & E-Commerce Development | Such Software",
|
title: "Custom Online Stores & E-Commerce Development | Such Software",
|
||||||
description: "Custom online stores with no upfront cost and revenue-share pricing. Own your data and escape template limits. Built on Medusa.",
|
description: "Custom online stores with no upfront cost and revenue-share pricing. Own your data and escape template limits. Built on Medusa.",
|
||||||
type: "website",
|
type: "website",
|
||||||
|
images: ["/images/og/webshops.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/webshops.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{ q: "How does revenue-share pricing work?", a: "Instead of a large upfront build fee, we build and host your store at our own cost and take a small percentage of sales once you are live. We only make money when you do." },
|
||||||
|
{ q: "What is headless commerce, and why Medusa?", a: "Headless means the storefront is decoupled from the commerce backend. We use Medusa v2 (open-source, Node.js) for the backend and Next.js for a fast custom storefront, so you are not boxed in by a template or a hosted platform's limits." },
|
||||||
|
{ q: "Do I own my data?", a: "Yes. Unlike hosted platforms, you own your customer data, product catalog, and order history outright. There is no platform lock-in." },
|
||||||
|
{ q: "Can you migrate my existing store?", a: "Yes. We can migrate catalogs, customers, and order history from Shopify, WooCommerce, and other platforms." },
|
||||||
|
{ q: "Which payment methods are supported?", a: "Stripe and PayPal out of the box, with optional cryptocurrency payments if you want them." },
|
||||||
|
];
|
||||||
|
|
||||||
export default function WebshopsPage() {
|
export default function WebshopsPage() {
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
|
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground">
|
||||||
@@ -25,6 +38,7 @@ export default function WebshopsPage() {
|
|||||||
{ name: "Products", path: "/products" },
|
{ name: "Products", path: "/products" },
|
||||||
{ name: "Custom Online Stores", path: "/products/webshops" },
|
{ name: "Custom Online Stores", path: "/products/webshops" },
|
||||||
])} />
|
])} />
|
||||||
|
<JsonLd data={faqLd(faqs)} />
|
||||||
<Header />
|
<Header />
|
||||||
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
|
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
|
||||||
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
|
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
|
||||||
@@ -85,6 +99,16 @@ export default function WebshopsPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
|
||||||
|
<div className="space-y-6 mb-12">
|
||||||
|
{faqs.map((f) => (
|
||||||
|
<div key={f.q}>
|
||||||
|
<h3 className="font-bold mb-1 text-emerald-600 dark:text-emerald-400">{f.q}</h3>
|
||||||
|
<p className="text-muted-foreground">{f.a}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Link href="/about#contact">
|
<Link href="/about#contact">
|
||||||
<Button size="lg">Get in Touch</Button>
|
<Button size="lg">Get in Touch</Button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { MobileNav } from "@/components/layout/mobile-nav";
|
|||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
import { WownerogueLinks } from "@/components/store-buttons";
|
import { WownerogueLinks } from "@/components/store-buttons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { JsonLd, breadcrumbLd } from "@/components/seo/json-ld";
|
import { JsonLd, breadcrumbLd, faqLd } from "@/components/seo/json-ld";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -25,9 +25,22 @@ export const metadata: Metadata = {
|
|||||||
description: "A provably-fair, blockchain-synced multiplayer roguelike. Escape the dungeon before the next block. Free in-browser at play.wowne.ro.",
|
description: "A provably-fair, blockchain-synced multiplayer roguelike. Escape the dungeon before the next block. Free in-browser at play.wowne.ro.",
|
||||||
type: "website",
|
type: "website",
|
||||||
url: "/products/wownerogue",
|
url: "/products/wownerogue",
|
||||||
|
images: ["/images/og/wownerogue.png"],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: ["/images/og/wownerogue.png"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{ q: "What is a provably-fair game?", a: "A game whose outcome can be cryptographically verified after the fact. Before your run, the server publishes a SHA-256 commitment of a secret seed; afterward it reveals the seed. You hash the seed and confirm it matches the commitment, which proves the dungeon was fixed before you started and not rigged against you." },
|
||||||
|
{ q: "Is Wownerogue free to play?", a: "Yes. play.wowne.ro is free to play, with optional paid entry for the Hall of Champions leaderboard. The monerogue.app instance runs on Monero stagenet, where coins have no real value." },
|
||||||
|
{ q: "Do I need a wallet or a download?", a: "No download: it runs in your browser. You only need a Monero or Wownero address if you want to receive a payout on the payout instance." },
|
||||||
|
{ q: "How does the block-timing deadline work?", a: "Each round ends on a new Monero or Wownero block. Block times are random (roughly two minutes on average), so your deadline to escape is never the same twice. That randomness is the core of the game." },
|
||||||
|
{ q: "Is Wownerogue open source?", a: "Yes, it is MIT-licensed. The full server and client are public on GitHub at github.com/Such-Software/wownerogue." },
|
||||||
|
];
|
||||||
|
|
||||||
// VideoGame structured data: the highest-value rich result for a game query.
|
// VideoGame structured data: the highest-value rich result for a game query.
|
||||||
const jsonLd = {
|
const jsonLd = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -77,6 +90,7 @@ export default function WowneroguePage() {
|
|||||||
{ name: "Wownerogue", path: "/products/wownerogue" },
|
{ name: "Wownerogue", path: "/products/wownerogue" },
|
||||||
])} />
|
])} />
|
||||||
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
|
||||||
|
<JsonLd data={faqLd(faqs)} />
|
||||||
<Header />
|
<Header />
|
||||||
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
|
<div id="main-content" className="z-10 w-full max-w-4xl mx-auto py-20 px-4 pb-24 md:pb-20">
|
||||||
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
|
<Link href="/products" className="text-sm text-muted-foreground hover:text-foreground mb-8 inline-block">
|
||||||
@@ -166,6 +180,16 @@ export default function WowneroguePage() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-bold mb-6">FAQ</h2>
|
||||||
|
<div className="space-y-6 mb-12">
|
||||||
|
{faqs.map((f) => (
|
||||||
|
<div key={f.q}>
|
||||||
|
<h3 className="font-bold mb-1 text-orange-600 dark:text-orange-400">{f.q}</h3>
|
||||||
|
<p className="text-muted-foreground">{f.a}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mb-12">
|
<p className="text-muted-foreground mb-12">
|
||||||
Want the build notes? Read{" "}
|
Want the build notes? Read{" "}
|
||||||
<Link href="/blog/wownerogue-launch" className="text-orange-600 dark:text-orange-400 hover:underline">
|
<Link href="/blog/wownerogue-launch" className="text-orange-600 dark:text-orange-400 hover:underline">
|
||||||
|
|||||||
@@ -6,8 +6,17 @@ import { ScrollReveal } from "@/components/scroll-reveal";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card } from "@repo/ui/components/card";
|
import { Card } from "@repo/ui/components/card";
|
||||||
|
import { JsonLd, faqLd } from "@/components/seo/json-ld";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
const faqs = [
|
||||||
|
{ q: "What does Such Software build?", a: "Custom e-commerce on Medusa v2, real-time web apps and multiplayer experiences, and cryptocurrency and Monero integrations and consulting. We work across the stack, from backend infrastructure to accessible front-ends." },
|
||||||
|
{ q: "Where are you based?", a: "We are a software studio in Kennett Square, Pennsylvania, working with clients remotely across the US and beyond." },
|
||||||
|
{ q: "Do you build for accessibility?", a: "Yes. We build to WCAG 2.2 and audit for accessibility and Core Web Vitals as part of every project, not as an afterthought." },
|
||||||
|
{ q: "How does a project start?", a: "Reach out through the contact form with what you need. We scope it, propose an approach, and from there it can run as a fixed project or a performance-based revenue-share partnership." },
|
||||||
|
{ q: "Do you take revenue-share work?", a: "Yes. For a limited number of partners we build and operate products (such as webshops) for a share of revenue instead of a large upfront fee, so our incentives line up with yours." },
|
||||||
|
];
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Medusa, Web App & Crypto Development Services | Such Software",
|
title: "Medusa, Web App & Crypto Development Services | Such Software",
|
||||||
description: "Custom e-commerce on Medusa v2, real-time web apps, and Monero/crypto consulting. A Kennett Square, PA studio building for scale, accessibility, and performance.",
|
description: "Custom e-commerce on Medusa v2, real-time web apps, and Monero/crypto consulting. A Kennett Square, PA studio building for scale, accessibility, and performance.",
|
||||||
@@ -24,6 +33,7 @@ export const metadata: Metadata = {
|
|||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
return (
|
return (
|
||||||
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden">
|
<main className="relative min-h-screen flex flex-col items-center bg-background text-foreground overflow-hidden">
|
||||||
|
<JsonLd data={faqLd(faqs)} />
|
||||||
<DataGrid />
|
<DataGrid />
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
@@ -170,6 +180,18 @@ export default function ServicesPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
|
<ScrollReveal className="mt-20">
|
||||||
|
<h2 className="text-3xl font-bold mb-8 tracking-tight">Frequently Asked Questions</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{faqs.map((f) => (
|
||||||
|
<div key={f.q}>
|
||||||
|
<h3 className="text-xl font-bold mb-1">{f.q}</h3>
|
||||||
|
<p className="text-muted-foreground leading-relaxed">{f.a}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollReveal>
|
||||||
|
|
||||||
<ScrollReveal className="mt-20 p-16 rounded-[2.5rem] bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group border-2 border-white/5">
|
<ScrollReveal className="mt-20 p-16 rounded-[2.5rem] bg-gradient-to-br from-slate-900 via-slate-800 to-indigo-950 text-white text-center shadow-2xl relative overflow-hidden group border-2 border-white/5">
|
||||||
<div className="absolute inset-0 bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
|
<div className="absolute inset-0 bg-blue-500/10 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
<h2 className="text-4xl font-bold mb-6 relative z-10 tracking-tight">Let's build together</h2>
|
<h2 className="text-4xl font-bold mb-6 relative z-10 tracking-tight">Let's build together</h2>
|
||||||
|
|||||||
@@ -4,30 +4,36 @@ import { getAllPosts } from "@/lib/blog";
|
|||||||
export default function sitemap(): MetadataRoute.Sitemap {
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
const base = "https://such.software";
|
const base = "https://such.software";
|
||||||
|
|
||||||
|
// Stable content-update date for the static pages. Using `new Date()` here would stamp
|
||||||
|
// every page as "modified now" on every build, which is noise that dilutes the freshness
|
||||||
|
// signal. Bump this when the site's static content meaningfully changes. (Blog posts carry
|
||||||
|
// their own per-post dates below.)
|
||||||
|
const updated = new Date("2026-06-01");
|
||||||
|
|
||||||
const staticPages: MetadataRoute.Sitemap = [
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
{ url: base, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
|
{ url: base, lastModified: updated, changeFrequency: "weekly", priority: 1.0 },
|
||||||
{ url: `${base}/products`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.9 },
|
{ url: `${base}/products`, lastModified: updated, changeFrequency: "weekly", priority: 0.9 },
|
||||||
{ url: `${base}/products/wownerogue`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/wownerogue`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/neroswap`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/neroswap`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/webshops`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/webshops`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/custom-websites`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/custom-websites`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/smirk-wallet`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/smirk-wallet`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/bauhaus-echo`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/bauhaus-echo`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/vegan-iq`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
|
{ url: `${base}/products/vegan-iq`, lastModified: updated, changeFrequency: "monthly", priority: 0.8 },
|
||||||
{ url: `${base}/products/suchoice`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
|
{ url: `${base}/products/suchoice`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
|
||||||
{ url: `${base}/products/such-moon-launch`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
|
{ url: `${base}/products/such-moon-launch`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
|
||||||
{ url: `${base}/services`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
|
{ url: `${base}/services`, lastModified: updated, changeFrequency: "monthly", priority: 0.9 },
|
||||||
{ url: `${base}/about`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
|
{ url: `${base}/about`, lastModified: updated, changeFrequency: "monthly", priority: 0.7 },
|
||||||
{ url: `${base}/support`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
|
{ url: `${base}/support`, lastModified: updated, changeFrequency: "monthly", priority: 0.5 },
|
||||||
{ url: `${base}/privacy`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 },
|
{ url: `${base}/privacy`, lastModified: updated, changeFrequency: "yearly", priority: 0.3 },
|
||||||
{ url: `${base}/terms`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 },
|
{ url: `${base}/terms`, lastModified: updated, changeFrequency: "yearly", priority: 0.3 },
|
||||||
{ url: `${base}/apps`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
|
{ url: `${base}/apps`, lastModified: updated, changeFrequency: "monthly", priority: 0.5 },
|
||||||
{ url: `${base}/blog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.8 },
|
{ url: `${base}/blog`, lastModified: updated, changeFrequency: "weekly", priority: 0.8 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const blogPosts: MetadataRoute.Sitemap = getAllPosts().map((post) => ({
|
const blogPosts: MetadataRoute.Sitemap = getAllPosts().map((post) => ({
|
||||||
url: `${base}/blog/${post.slug}`,
|
url: `${base}/blog/${post.slug}`,
|
||||||
lastModified: post.date ? new Date(post.date + "T12:00:00") : new Date(),
|
lastModified: post.date ? new Date(post.date + "T12:00:00") : updated,
|
||||||
changeFrequency: "monthly",
|
changeFrequency: "monthly",
|
||||||
priority: 0.6,
|
priority: 0.6,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ export function softwareAppLd(opts: SoftwareAppOpts) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** FAQPage node. Pair it with the SAME questions/answers rendered visibly on the page. */
|
||||||
|
export function faqLd(items: { q: string; a: string }[]) {
|
||||||
|
return {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "FAQPage",
|
||||||
|
mainEntity: items.map((it) => ({
|
||||||
|
"@type": "Question",
|
||||||
|
name: it.q,
|
||||||
|
acceptedAnswer: { "@type": "Answer", text: it.a },
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** BlogPosting node for an article page. */
|
/** BlogPosting node for an article page. */
|
||||||
export function articleLd(opts: { title: string; description: string; slug: string; date: string }) {
|
export function articleLd(opts: { title: string; description: string; slug: string; date: string }) {
|
||||||
const url = `${SITE}/blog/${opts.slug}`;
|
const url = `${SITE}/blog/${opts.slug}`;
|
||||||
|
|||||||