@m13v/seo-components 0.20.1 → 0.22.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/package.json +1 -1
- package/src/components/BookCallCTA.tsx +23 -5
- package/src/components/InlineCta.tsx +14 -2
- package/src/components/StickyBottomCta.tsx +13 -2
- package/src/index.ts +1 -0
- package/src/lib/generate-robots.ts +90 -0
- package/src/lib/guide-context.ts +19 -2
- package/src/lib/track.ts +48 -0
- package/src/server.ts +7 -0
package/README.md
CHANGED
|
@@ -110,6 +110,8 @@ import { walkPages, createGuideChatHandler, logAiUsage } from "@seo/components/s
|
|
|
110
110
|
| Export | What it does |
|
|
111
111
|
|--------|-------------|
|
|
112
112
|
| `walkPages()` | Discovers all pages in `src/app/`, extracts titles, descriptions, H2 sections |
|
|
113
|
+
| `generateSitemap()` | Returns a `MetadataRoute.Sitemap` by walking `src/app` with priority tiers |
|
|
114
|
+
| `generateRobots()` | Returns a `MetadataRoute.Robots` with default + AI-crawler allowlist + sitemap URL |
|
|
113
115
|
| `createGuideChatHandler()` | Gemini streaming chat route handler |
|
|
114
116
|
| `discoverGuides()` | Legacy guide discovery (delegates to `walkPages`) |
|
|
115
117
|
| `getGuideContext()` | Builds page context for AI chat |
|
|
@@ -117,6 +119,48 @@ import { walkPages, createGuideChatHandler, logAiUsage } from "@seo/components/s
|
|
|
117
119
|
| `getSupabaseAdmin()` | Supabase admin client |
|
|
118
120
|
| `slugify()` | URL-safe slug utility |
|
|
119
121
|
|
|
122
|
+
### Sitemap + robots (canonical setup)
|
|
123
|
+
|
|
124
|
+
Every site should ship a **dynamic** sitemap and a robots.txt that references it. The library provides both as one-liners so you never hand-roll either again.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// src/app/sitemap.ts
|
|
128
|
+
import type { MetadataRoute } from "next";
|
|
129
|
+
import { generateSitemap } from "@seo/components/server";
|
|
130
|
+
|
|
131
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
132
|
+
return generateSitemap({ baseUrl: "https://yourdomain.com" });
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// src/app/robots.ts
|
|
138
|
+
import type { MetadataRoute } from "next";
|
|
139
|
+
import { generateRobots } from "@seo/components/server";
|
|
140
|
+
|
|
141
|
+
export default function robots(): MetadataRoute.Robots {
|
|
142
|
+
return generateRobots({ baseUrl: "https://yourdomain.com" });
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`generateSitemap()` walks `src/app/` for `page.tsx` files (skipping route groups, dynamic segments, `api/`, underscore-prefixed dirs) and applies priority tiers. For dynamic routes like `/blog/[slug]`, pass `extraEntries`:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
return generateSitemap({
|
|
150
|
+
baseUrl: "https://yourdomain.com",
|
|
151
|
+
extraEntries: posts.map((p) => ({ url: `https://yourdomain.com/blog/${p.slug}` })),
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`generateRobots()` emits the default `User-agent: *` rule, a 13-bot AI allowlist (GPTBot, ChatGPT-User, ClaudeBot, Claude-Web, anthropic-ai, PerplexityBot, CCBot, Google-Extended, Bytespider, cohere-ai, Applebot, Applebot-Extended), and a sitemap URL derived from `baseUrl`. Override `aiAllowlist`, `disallow`, `extraRules`, or `sitemap` as needed.
|
|
156
|
+
|
|
157
|
+
Verify live after deploy:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
curl -s -o /dev/null -w "%{http_code}\n" https://yourdomain.com/sitemap.xml # 200
|
|
161
|
+
curl -s https://yourdomain.com/robots.txt | grep sitemap.xml # non-empty
|
|
162
|
+
```
|
|
163
|
+
|
|
120
164
|
## JSON-LD Helpers
|
|
121
165
|
|
|
122
166
|
```tsx
|
package/package.json
CHANGED
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
-
import { trackScheduleClick } from "../lib/track";
|
|
5
|
+
import { trackScheduleClick, withBookingAttribution } from "../lib/track";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render the bare destination for SSR, then swap in the UTM-attributed URL
|
|
9
|
+
* after hydration so Cmd+Click / middle-click preserves the attribution.
|
|
10
|
+
* PostHog's `schedule_click` event still receives the bare destination so
|
|
11
|
+
* aggregation isn't fragmented per page.
|
|
12
|
+
*/
|
|
13
|
+
function useBookingHref(destination: string): string {
|
|
14
|
+
const [href, setHref] = useState(destination);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setHref(withBookingAttribution(destination));
|
|
17
|
+
}, [destination]);
|
|
18
|
+
return href;
|
|
19
|
+
}
|
|
6
20
|
|
|
7
21
|
export type BookCallAppearance = "inline" | "sticky" | "hero" | "footer";
|
|
8
22
|
|
|
@@ -111,6 +125,7 @@ function InlineBookCall({
|
|
|
111
125
|
section: string;
|
|
112
126
|
site?: string;
|
|
113
127
|
}) {
|
|
128
|
+
const href = useBookingHref(destination);
|
|
114
129
|
return (
|
|
115
130
|
<motion.div
|
|
116
131
|
className="my-12 mx-auto max-w-2xl p-6 rounded-2xl border border-teal-100 dark:border-teal-800/60 bg-teal-50/40 dark:bg-teal-950/60"
|
|
@@ -126,7 +141,7 @@ function InlineBookCall({
|
|
|
126
141
|
{description}
|
|
127
142
|
</p>
|
|
128
143
|
<a
|
|
129
|
-
href={
|
|
144
|
+
href={href}
|
|
130
145
|
target="_blank"
|
|
131
146
|
rel="noopener noreferrer"
|
|
132
147
|
className="inline-flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-lg bg-teal-500 text-accent-contrast hover:bg-accent-dim transition-colors"
|
|
@@ -151,9 +166,10 @@ function HeroBookCall({
|
|
|
151
166
|
section: string;
|
|
152
167
|
site?: string;
|
|
153
168
|
}) {
|
|
169
|
+
const href = useBookingHref(destination);
|
|
154
170
|
return (
|
|
155
171
|
<a
|
|
156
|
-
href={
|
|
172
|
+
href={href}
|
|
157
173
|
target="_blank"
|
|
158
174
|
rel="noopener noreferrer"
|
|
159
175
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-teal-200 dark:border-teal-800/60 text-sm font-medium text-teal-700 dark:text-teal-200 hover:bg-teal-50 dark:hover:bg-teal-950/60 transition-colors"
|
|
@@ -181,6 +197,7 @@ function FooterBookCall({
|
|
|
181
197
|
section: string;
|
|
182
198
|
site?: string;
|
|
183
199
|
}) {
|
|
200
|
+
const href = useBookingHref(destination);
|
|
184
201
|
return (
|
|
185
202
|
<motion.div
|
|
186
203
|
className="my-16 mx-auto max-w-3xl p-8 rounded-2xl border border-teal-200 dark:border-teal-800/60 bg-gradient-to-br from-teal-50 to-white dark:from-teal-950/70 dark:to-zinc-950 text-center"
|
|
@@ -196,7 +213,7 @@ function FooterBookCall({
|
|
|
196
213
|
{description}
|
|
197
214
|
</p>
|
|
198
215
|
<a
|
|
199
|
-
href={
|
|
216
|
+
href={href}
|
|
200
217
|
target="_blank"
|
|
201
218
|
rel="noopener noreferrer"
|
|
202
219
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-teal-500 text-accent-contrast hover:bg-accent-dim transition-colors text-sm font-medium"
|
|
@@ -226,6 +243,7 @@ function StickyBookCall({
|
|
|
226
243
|
scrollThreshold: number;
|
|
227
244
|
}) {
|
|
228
245
|
const [visible, setVisible] = useState(false);
|
|
246
|
+
const href = useBookingHref(destination);
|
|
229
247
|
|
|
230
248
|
useEffect(() => {
|
|
231
249
|
const handler = () => setVisible(window.scrollY > scrollThreshold);
|
|
@@ -249,7 +267,7 @@ function StickyBookCall({
|
|
|
249
267
|
{description}
|
|
250
268
|
</p>
|
|
251
269
|
<a
|
|
252
|
-
href={
|
|
270
|
+
href={href}
|
|
253
271
|
target="_blank"
|
|
254
272
|
rel="noopener noreferrer"
|
|
255
273
|
className="text-sm font-medium px-4 py-2 rounded-lg bg-teal-500 text-accent-contrast hover:bg-accent-dim transition-colors"
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
3
4
|
import { motion } from "framer-motion";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
trackGetStartedClick,
|
|
7
|
+
trackScheduleClick,
|
|
8
|
+
withBookingAttribution,
|
|
9
|
+
} from "../lib/track";
|
|
5
10
|
|
|
6
11
|
interface InlineCtaProps {
|
|
7
12
|
heading: string;
|
|
@@ -30,6 +35,13 @@ export function InlineCta({
|
|
|
30
35
|
site,
|
|
31
36
|
section,
|
|
32
37
|
}: InlineCtaProps) {
|
|
38
|
+
// For schedule CTAs, rewrite the href after hydration so the Cal.com webhook
|
|
39
|
+
// sees `metadata[utm_*]` params and can attribute the booking to this page.
|
|
40
|
+
// PostHog events still carry the bare `href` so aggregation stays clean.
|
|
41
|
+
const [renderHref, setRenderHref] = useState(href);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
setRenderHref(trackAs === "schedule" ? withBookingAttribution(href) : href);
|
|
44
|
+
}, [href, trackAs]);
|
|
33
45
|
return (
|
|
34
46
|
<motion.div
|
|
35
47
|
className="my-12 mx-auto max-w-2xl p-6 rounded-2xl border border-teal-100 dark:border-teal-800/60 bg-teal-50/30 dark:bg-teal-950/60"
|
|
@@ -45,7 +57,7 @@ export function InlineCta({
|
|
|
45
57
|
{body}
|
|
46
58
|
</p>
|
|
47
59
|
<a
|
|
48
|
-
href={
|
|
60
|
+
href={renderHref}
|
|
49
61
|
className="inline-flex items-center gap-2 text-sm font-medium text-teal-600 dark:text-teal-300 hover:text-accent-dim transition-colors"
|
|
50
62
|
onClick={() => {
|
|
51
63
|
if (trackAs === "get_started") {
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { motion, AnimatePresence } from "framer-motion";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
trackGetStartedClick,
|
|
7
|
+
trackScheduleClick,
|
|
8
|
+
withBookingAttribution,
|
|
9
|
+
} from "../lib/track";
|
|
6
10
|
|
|
7
11
|
interface StickyBottomCtaProps {
|
|
8
12
|
description: string;
|
|
@@ -33,6 +37,13 @@ export function StickyBottomCta({
|
|
|
33
37
|
}: StickyBottomCtaProps) {
|
|
34
38
|
const [visible, setVisible] = useState(false);
|
|
35
39
|
|
|
40
|
+
// Rewrite schedule CTAs' href after hydration so the Cal.com webhook sees
|
|
41
|
+
// `metadata[utm_*]` params and attributes the booking to this page.
|
|
42
|
+
const [renderHref, setRenderHref] = useState(href);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setRenderHref(trackAs === "schedule" ? withBookingAttribution(href) : href);
|
|
45
|
+
}, [href, trackAs]);
|
|
46
|
+
|
|
36
47
|
useEffect(() => {
|
|
37
48
|
const handler = () => setVisible(window.scrollY > scrollThreshold);
|
|
38
49
|
window.addEventListener("scroll", handler);
|
|
@@ -75,7 +86,7 @@ export function StickyBottomCta({
|
|
|
75
86
|
{description}
|
|
76
87
|
</p>
|
|
77
88
|
<a
|
|
78
|
-
href={
|
|
89
|
+
href={renderHref}
|
|
79
90
|
className="text-sm font-medium px-4 py-2 rounded-lg bg-teal-500 text-accent-contrast hover:bg-accent-dim transition-colors"
|
|
80
91
|
onClick={onClick}
|
|
81
92
|
>
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type RobotsRule = {
|
|
2
|
+
userAgent: string | string[];
|
|
3
|
+
allow?: string | string[];
|
|
4
|
+
disallow?: string | string[];
|
|
5
|
+
crawlDelay?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type GenerateRobotsOptions = {
|
|
9
|
+
/** Absolute base URL, no trailing slash (e.g. `https://fde10x.com`). Used to build the sitemap URL. */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* Paths disallowed for the default `*` rule. Defaults to `["/api/"]`.
|
|
13
|
+
* Pass an empty array to allow everything.
|
|
14
|
+
*/
|
|
15
|
+
disallow?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Explicit AI crawler allowlist. Each user agent gets its own rule with
|
|
18
|
+
* `allow: "/"` so operators (Google, Perplexity, etc.) can distinguish
|
|
19
|
+
* this site as AI-opted-in. Defaults to the canonical 13-bot allowlist
|
|
20
|
+
* used across every m13v property. Pass `[]` to disable.
|
|
21
|
+
*/
|
|
22
|
+
aiAllowlist?: string[];
|
|
23
|
+
/** Extra rules appended after the default + AI allowlist. */
|
|
24
|
+
extraRules?: RobotsRule[];
|
|
25
|
+
/**
|
|
26
|
+
* Override the sitemap URL. Defaults to `${baseUrl}/sitemap.xml`. Pass an
|
|
27
|
+
* array when you ship multiple sitemaps.
|
|
28
|
+
*/
|
|
29
|
+
sitemap?: string | string[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type GeneratedRobots = {
|
|
33
|
+
rules: RobotsRule[];
|
|
34
|
+
sitemap: string | string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DEFAULT_AI_ALLOWLIST = [
|
|
38
|
+
"GPTBot",
|
|
39
|
+
"ChatGPT-User",
|
|
40
|
+
"ClaudeBot",
|
|
41
|
+
"Claude-Web",
|
|
42
|
+
"anthropic-ai",
|
|
43
|
+
"PerplexityBot",
|
|
44
|
+
"CCBot",
|
|
45
|
+
"Google-Extended",
|
|
46
|
+
"Bytespider",
|
|
47
|
+
"cohere-ai",
|
|
48
|
+
"Applebot",
|
|
49
|
+
"Applebot-Extended",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const DEFAULT_DISALLOW = ["/api/"];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate a Next.js `MetadataRoute.Robots` value with a default rule, an
|
|
56
|
+
* AI-crawler allowlist, and a sitemap reference derived from `baseUrl`.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* // src/app/robots.ts
|
|
60
|
+
* import { generateRobots } from "@seo/components/server";
|
|
61
|
+
*
|
|
62
|
+
* export default function robots() {
|
|
63
|
+
* return generateRobots({ baseUrl: "https://fde10x.com" });
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Pair with `generateSitemap()` in `src/app/sitemap.ts` — the two together
|
|
68
|
+
* are the canonical SEO-infrastructure setup for a client site.
|
|
69
|
+
*/
|
|
70
|
+
export function generateRobots(opts: GenerateRobotsOptions): GeneratedRobots {
|
|
71
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
72
|
+
const disallow = opts.disallow ?? DEFAULT_DISALLOW;
|
|
73
|
+
const aiAllowlist = opts.aiAllowlist ?? DEFAULT_AI_ALLOWLIST;
|
|
74
|
+
|
|
75
|
+
const defaultRule: RobotsRule = {
|
|
76
|
+
userAgent: "*",
|
|
77
|
+
allow: "/",
|
|
78
|
+
...(disallow.length > 0 ? { disallow } : {}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const aiRules: RobotsRule[] = aiAllowlist.map((userAgent) => ({
|
|
82
|
+
userAgent,
|
|
83
|
+
allow: "/",
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const rules = [defaultRule, ...aiRules, ...(opts.extraRules ?? [])];
|
|
87
|
+
const sitemap = opts.sitemap ?? `${baseUrl}/sitemap.xml`;
|
|
88
|
+
|
|
89
|
+
return { rules, sitemap };
|
|
90
|
+
}
|
package/src/lib/guide-context.ts
CHANGED
|
@@ -24,7 +24,11 @@ export function getGuideContext(
|
|
|
24
24
|
);
|
|
25
25
|
if (!guide) return null;
|
|
26
26
|
|
|
27
|
-
//
|
|
27
|
+
// Resolve the page file on disk. `guide.href` is a URL path, which has
|
|
28
|
+
// Next.js route-group segments like `(content)` stripped out, so we cannot
|
|
29
|
+
// just join `appDir + href`. Instead, compute the URL prefix that `dir`
|
|
30
|
+
// maps to, strip it from `href`, and append the remainder to `dir` (the
|
|
31
|
+
// real filesystem path the caller passed in).
|
|
28
32
|
const relDir = path.relative(process.cwd(), dir);
|
|
29
33
|
const relParts = relDir.split(path.sep);
|
|
30
34
|
const appIdx = relParts.indexOf("app");
|
|
@@ -32,7 +36,20 @@ export function getGuideContext(
|
|
|
32
36
|
appIdx >= 0
|
|
33
37
|
? path.join(process.cwd(), ...relParts.slice(0, appIdx + 1))
|
|
34
38
|
: dir;
|
|
35
|
-
const
|
|
39
|
+
const relContent = path.relative(appDir, dir);
|
|
40
|
+
const urlSegments = relContent
|
|
41
|
+
? relContent
|
|
42
|
+
.split(path.sep)
|
|
43
|
+
.filter((s) => !(s.startsWith("(") && s.endsWith(")")))
|
|
44
|
+
: [];
|
|
45
|
+
const urlPrefix = urlSegments.length ? "/" + urlSegments.join("/") : "";
|
|
46
|
+
let relHref = guide.href;
|
|
47
|
+
if (urlPrefix && relHref.startsWith(urlPrefix + "/")) {
|
|
48
|
+
relHref = relHref.slice(urlPrefix.length + 1);
|
|
49
|
+
} else if (relHref.startsWith("/")) {
|
|
50
|
+
relHref = relHref.slice(1);
|
|
51
|
+
}
|
|
52
|
+
const pagePath = path.join(dir, relHref, "page.tsx");
|
|
36
53
|
let rawSource = "";
|
|
37
54
|
try {
|
|
38
55
|
rawSource = fs.readFileSync(pagePath, "utf-8");
|
package/src/lib/track.ts
CHANGED
|
@@ -8,6 +8,54 @@
|
|
|
8
8
|
|
|
9
9
|
import { captureFromWindow } from "./analytics-context";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Rewrite a booking URL (Cal.com, Calendly, etc.) so the booking webhook can
|
|
13
|
+
* attribute the booking back to the specific landing page it came from.
|
|
14
|
+
*
|
|
15
|
+
* Cal.com's hosted booking page captures plain `utm_source/utm_medium/utm_campaign`
|
|
16
|
+
* URL query params and stores them on the booking's metadata, which Cal.com
|
|
17
|
+
* then forwards in the webhook payload as `booking.metadata.utm_*`. We also
|
|
18
|
+
* emit Cal.com's bracketed `metadata[key]=value` form as a belt-and-braces
|
|
19
|
+
* fallback (some Cal.com surfaces accept that syntax instead). Our webhook
|
|
20
|
+
* handler (social-autoposter-website/api/webhooks/cal/route.ts) writes
|
|
21
|
+
* `booking.metadata.utm_*` into `cal_bookings.utm_source / utm_medium /
|
|
22
|
+
* utm_campaign`, so every booking carries the originating site + page path.
|
|
23
|
+
*
|
|
24
|
+
* Attribution scheme:
|
|
25
|
+
* utm_source = current hostname (e.g. "fazm.com")
|
|
26
|
+
* utm_medium = "schedule_click"
|
|
27
|
+
* utm_campaign = current pathname (e.g. "/t/how-to-quit-weed")
|
|
28
|
+
*
|
|
29
|
+
* SSR-safe: returns the URL unchanged on the server (no `window`) or when the
|
|
30
|
+
* input cannot be parsed as a URL. Never overwrites a pre-existing query key
|
|
31
|
+
* so manual overrides on specific CTAs still win.
|
|
32
|
+
*
|
|
33
|
+
* Applied automatically by `BookCallCTA`, and by `InlineCta` /
|
|
34
|
+
* `StickyBottomCta` when `trackAs === "schedule"`. Consumers that build a
|
|
35
|
+
* custom Book-a-Call CTA MUST route their href through this helper or
|
|
36
|
+
* page-level booking attribution breaks.
|
|
37
|
+
*/
|
|
38
|
+
export function withBookingAttribution(destination: string): string {
|
|
39
|
+
if (typeof window === "undefined") return destination;
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(destination, window.location.href);
|
|
42
|
+
const source = window.location.hostname;
|
|
43
|
+
const campaign = window.location.pathname || "/";
|
|
44
|
+
const setIfAbsent = (key: string, value: string) => {
|
|
45
|
+
if (!url.searchParams.has(key)) url.searchParams.set(key, value);
|
|
46
|
+
};
|
|
47
|
+
setIfAbsent("utm_source", source);
|
|
48
|
+
setIfAbsent("utm_medium", "schedule_click");
|
|
49
|
+
setIfAbsent("utm_campaign", campaign);
|
|
50
|
+
setIfAbsent("metadata[utm_source]", source);
|
|
51
|
+
setIfAbsent("metadata[utm_medium]", "schedule_click");
|
|
52
|
+
setIfAbsent("metadata[utm_campaign]", campaign);
|
|
53
|
+
return url.toString();
|
|
54
|
+
} catch {
|
|
55
|
+
return destination;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
11
59
|
export interface ScheduleClickProps {
|
|
12
60
|
/** Absolute URL the click sends the user to (e.g. https://cal.com/team/mediar/demo). */
|
|
13
61
|
destination: string;
|
package/src/server.ts
CHANGED
|
@@ -11,6 +11,13 @@ export type {
|
|
|
11
11
|
GenerateSitemapOptions,
|
|
12
12
|
} from "./lib/generate-sitemap";
|
|
13
13
|
|
|
14
|
+
export { generateRobots } from "./lib/generate-robots";
|
|
15
|
+
export type {
|
|
16
|
+
RobotsRule,
|
|
17
|
+
GenerateRobotsOptions,
|
|
18
|
+
GeneratedRobots,
|
|
19
|
+
} from "./lib/generate-robots";
|
|
20
|
+
|
|
14
21
|
export { slugify } from "./lib/slugify";
|
|
15
22
|
|
|
16
23
|
export { createGuideChatHandler } from "./lib/guide-chat-route";
|