feat: search metadata, structured data and indexing rules
Only the production deployment may be indexed; testing and preview builds answer with noindex and a closed robots file. The homepage now describes the person, the services and the FAQ as structured data, and title, description and the 404 page are written for what people search for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,41 +1,15 @@
|
||||
import * as React from "react";
|
||||
|
||||
const QUESTIONS = [
|
||||
{
|
||||
q: "I am not technical. Is that a problem?",
|
||||
a: "No. I explain what I am doing in plain language, and you can ask at any time. You do not need to read code.",
|
||||
},
|
||||
{
|
||||
q: "How long am I committed?",
|
||||
a: "Project work is billed by effort. Either side can end it with two weeks notice to the end of a month.",
|
||||
},
|
||||
{
|
||||
q: "Who owns what you build?",
|
||||
a: "You do. Code and design belong to you, and you can hand them to another developer.",
|
||||
},
|
||||
{
|
||||
q: "Can you work with our existing team and code?",
|
||||
a: "Yes. I worked in agencies and product teams for years, so joining an existing project is familiar to me.",
|
||||
},
|
||||
{
|
||||
q: "How do I reach you?",
|
||||
a: "By email or video call. I work remotely and am available during European business hours.",
|
||||
},
|
||||
{
|
||||
q: "What do you not do?",
|
||||
a: "Mobile apps for the app stores, Windows software and running your servers. If your project needs those, I say so in the first call.",
|
||||
},
|
||||
] as const;
|
||||
import { FAQ } from "@/src/content/faq";
|
||||
|
||||
export const Faq: React.FC = () => (
|
||||
<div className="grid grid-cols-1 gap-x-12 md:grid-cols-2">
|
||||
{QUESTIONS.map((item) => (
|
||||
{FAQ.map((item) => (
|
||||
<details
|
||||
key={item.q}
|
||||
key={item.question}
|
||||
className="group border-t border-slate-200 py-6 open:pb-8"
|
||||
>
|
||||
<summary className="flex cursor-pointer list-none items-start justify-between gap-6 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl [&::-webkit-details-marker]:hidden">
|
||||
{item.q}
|
||||
{item.question}
|
||||
<span
|
||||
className="mt-1 shrink-0 font-mono text-xl text-slate-400 transition-transform duration-300 group-open:rotate-45"
|
||||
aria-hidden="true"
|
||||
@@ -44,7 +18,7 @@ export const Faq: React.FC = () => (
|
||||
</span>
|
||||
</summary>
|
||||
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500 md:text-lg">
|
||||
{item.a}
|
||||
{item.answer}
|
||||
</p>
|
||||
</details>
|
||||
))}
|
||||
|
||||
@@ -24,7 +24,7 @@ export const HeroStatement: React.FC = () => (
|
||||
|
||||
<div className="mt-10 grid grid-cols-1 gap-10 md:mt-14 md:grid-cols-12 md:items-end md:gap-12">
|
||||
<p className="m-0 max-w-2xl font-sans text-xl leading-snug text-slate-500 md:col-span-7 md:text-3xl">
|
||||
Freelance developer with 15 years of experience.{" "}
|
||||
Freelance web developer with 15 years of experience.{" "}
|
||||
<span className="text-slate-900">
|
||||
I build web apps, interfaces and websites.
|
||||
</span>
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
import { TECH } from "@/src/content/about";
|
||||
import { FAQ } from "@/src/content/faq";
|
||||
import { SERVICE_PATHS } from "@/src/content/paths";
|
||||
import {
|
||||
JOB_TITLE,
|
||||
OWNER,
|
||||
PROFILES,
|
||||
SITE_DESCRIPTION,
|
||||
SITE_LANGUAGE,
|
||||
SITE_NAME,
|
||||
SITE_URL,
|
||||
} from "@/src/content/site";
|
||||
import {
|
||||
buildStructuredData,
|
||||
serializeJsonLd,
|
||||
} from "@/src/domain/structured-data";
|
||||
|
||||
const STRUCTURED_DATA = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfessionalService",
|
||||
name: SITE_NAME,
|
||||
url: SITE_URL,
|
||||
const [postalCode = "", ...locality] = OWNER.city.split(" ");
|
||||
|
||||
const STRUCTURED_DATA = buildStructuredData({
|
||||
siteUrl: SITE_URL,
|
||||
siteName: SITE_NAME,
|
||||
language: SITE_LANGUAGE,
|
||||
description: SITE_DESCRIPTION,
|
||||
image: `${SITE_URL}/marc-mintel.png`,
|
||||
imageUrl: `${SITE_URL}/marc-mintel.png`,
|
||||
person: {
|
||||
name: OWNER.name,
|
||||
jobTitle: JOB_TITLE,
|
||||
profiles: PROFILES,
|
||||
knowsAbout: [
|
||||
"Web applications",
|
||||
"User interfaces",
|
||||
"Websites",
|
||||
"Process automation",
|
||||
"Web3 frontends",
|
||||
...TECH.flatMap((group) => group.items),
|
||||
],
|
||||
},
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress: OWNER.street,
|
||||
postalCode: OWNER.city.split(" ")[0],
|
||||
addressLocality: OWNER.city.split(" ").slice(1).join(" "),
|
||||
addressCountry: "DE",
|
||||
street: OWNER.street,
|
||||
postalCode,
|
||||
locality: locality.join(" "),
|
||||
countryCode: "DE",
|
||||
},
|
||||
founder: {
|
||||
"@type": "Person",
|
||||
name: SITE_NAME,
|
||||
jobTitle: "Freelance developer",
|
||||
url: SITE_URL,
|
||||
},
|
||||
knowsAbout: [
|
||||
"Web applications",
|
||||
"User interfaces",
|
||||
"Websites",
|
||||
"Process automation",
|
||||
"Web3 frontends",
|
||||
],
|
||||
} as const;
|
||||
services: SERVICE_PATHS.map((path) => ({
|
||||
name: path.service,
|
||||
description: path.intro,
|
||||
})),
|
||||
faq: FAQ,
|
||||
});
|
||||
|
||||
/** Serialized server-side from constants only; "<" is escaped to keep it inert inside a script tag. */
|
||||
/** Built server-side from the page content; see src/domain/structured-data.ts. */
|
||||
export const HomeJsonLd: React.FC = () => (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(STRUCTURED_DATA).replace(/</g, "\\u003c"),
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: serializeJsonLd(STRUCTURED_DATA) }}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user