@rimelight/ui 0.0.1 → 0.0.3
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/package.json +53 -49
- package/src/components/carousel/carousel-script.ts +188 -188
- package/src/components/carousel/index.ts +1 -1
- package/src/components/input-otp/InputOtpTypes.ts +6 -6
- package/src/components/radio-group/RadioGroupTypes.ts +6 -6
- package/src/components/select/SelectTypes.ts +13 -13
- package/src/components/switch/SwitchTypes.ts +6 -6
- package/src/components/toast/index.ts +1 -1
- package/src/components/toast/toast-manager.ts +234 -234
- package/src/components/toggle/ToggleTypes.ts +14 -14
- package/src/config/cookies.ts +5 -5
- package/src/config/index.ts +3 -3
- package/src/config/links.ts +4 -4
- package/src/config/site.config.ts +45 -45
- package/src/domain/cms/index.ts +1 -1
- package/src/domain/cms/types.ts +20 -20
- package/src/domain/i18n/constants.ts +21 -21
- package/src/domain/i18n/cookie-storage.ts +13 -13
- package/src/domain/i18n/country-to-locale-map.ts +67 -67
- package/src/domain/i18n/fetcher.ts +268 -268
- package/src/domain/i18n/flags.ts +16 -16
- package/src/domain/i18n/format.ts +90 -90
- package/src/domain/i18n/index.ts +10 -10
- package/src/domain/i18n/middleware/i18n.ts +160 -160
- package/src/domain/i18n/resolve-locale.ts +55 -55
- package/src/domain/i18n/schema.ts +29 -29
- package/src/domain/index.ts +4 -4
- package/src/domain/seo/constants.ts +20 -20
- package/src/domain/seo/index.ts +3 -3
- package/src/domain/seo/services/index.ts +3 -3
- package/src/domain/seo/services/llms.ts +53 -53
- package/src/domain/seo/services/robots.ts +12 -12
- package/src/domain/seo/services/sitemap.ts +56 -56
- package/src/domain/seo/utils.ts +37 -37
- package/src/domain/theme/constants.ts +6 -6
- package/src/domain/theme/index.ts +2 -2
- package/src/domain/theme/schema.ts +10 -10
- package/src/env.d.ts +49 -49
- package/src/integrations/index.ts +1 -0
- package/src/integrations/sri.ts +92 -0
- package/src/lib/component-preview.ts +73 -73
- package/src/lib/index.ts +6 -6
- package/src/lib/remark/remark-modified-time.ts +13 -13
- package/src/lib/showcase-preview-elements.ts +408 -408
- package/src/lib/showcase-renderer.ts +218 -218
- package/src/lib/showcase-utils.ts +189 -189
- package/src/lib/turnstile.ts +35 -35
- package/src/lib/utils/starwind/positioning.ts +318 -318
- package/src/locales/de/blog.json +9 -9
- package/src/locales/de/common.json +19 -19
- package/src/locales/de/errors.json +6 -6
- package/src/locales/de/home.json +20 -20
- package/src/locales/de/messages.json +8 -8
- package/src/locales/de/nav.json +13 -13
- package/src/locales/de/projects.json +9 -9
- package/src/locales/de/services.json +43 -43
- package/src/locales/en/blog.json +9 -9
- package/src/locales/en/chat.json +29 -29
- package/src/locales/en/common.json +19 -19
- package/src/locales/en/cookie_notice.json +11 -11
- package/src/locales/en/editor.json +24 -24
- package/src/locales/en/errors.json +6 -6
- package/src/locales/en/home.json +20 -20
- package/src/locales/en/messages.json +8 -8
- package/src/locales/en/nav.json +13 -13
- package/src/locales/en/projects.json +9 -9
- package/src/locales/en/services.json +43 -43
- package/src/locales/es/blog.json +9 -9
- package/src/locales/es/common.json +19 -19
- package/src/locales/es/errors.json +6 -6
- package/src/locales/es/home.json +20 -20
- package/src/locales/es/messages.json +8 -8
- package/src/locales/es/nav.json +13 -13
- package/src/locales/es/projects.json +9 -9
- package/src/locales/es/services.json +43 -43
- package/src/locales/ja/blog.json +9 -9
- package/src/locales/ja/common.json +19 -19
- package/src/locales/ja/errors.json +6 -6
- package/src/locales/ja/home.json +20 -20
- package/src/locales/ja/messages.json +8 -8
- package/src/locales/ja/nav.json +13 -13
- package/src/locales/ja/projects.json +9 -9
- package/src/locales/ja/services.json +43 -43
- package/src/locales/pt/blog.json +9 -9
- package/src/locales/pt/chat.json +29 -29
- package/src/locales/pt/common.json +19 -19
- package/src/locales/pt/cookie_notice.json +11 -11
- package/src/locales/pt/editor.json +24 -24
- package/src/locales/pt/errors.json +6 -6
- package/src/locales/pt/home.json +20 -20
- package/src/locales/pt/messages.json +8 -8
- package/src/locales/pt/nav.json +13 -13
- package/src/locales/pt/projects.json +9 -9
- package/src/locales/pt/services.json +43 -43
- package/src/middleware/index.ts +1 -0
- package/src/middleware/sri.ts +76 -0
- package/src/utils/index.ts +2 -2
- package/src/utils/server/cookies.ts +98 -98
- package/src/utils/server/index.ts +1 -1
- package/src/utils/shared/deep-merge.ts +36 -36
- package/src/utils/shared/embed.ts +105 -105
- package/src/utils/shared/index.ts +3 -3
- package/src/utils/shared/parse-boolean.ts +14 -14
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { z } from "astro/zod"
|
|
2
|
-
|
|
3
|
-
import { SUPPORTED_LOCALES } from "./constants"
|
|
4
|
-
import { NAMESPACE_KEYS } from "./runtime-constants"
|
|
5
|
-
|
|
6
|
-
// Auto-generated namespaces and real supported locales that have translations
|
|
7
|
-
export * from "./runtime-constants"
|
|
8
|
-
|
|
9
|
-
// Locale schema from shared constant
|
|
10
|
-
export const LocaleSchema = z.enum(SUPPORTED_LOCALES)
|
|
11
|
-
|
|
12
|
-
// Runtime safety: if namespaces are empty (fresh clone), fall back to string
|
|
13
|
-
const hasNamespaces = (NAMESPACE_KEYS as readonly string[]).length > 0
|
|
14
|
-
|
|
15
|
-
export const NamespaceSchema = hasNamespaces ? z.enum(NAMESPACE_KEYS) : z.string()
|
|
16
|
-
|
|
17
|
-
export type Locale = z.infer<typeof LocaleSchema>
|
|
18
|
-
|
|
19
|
-
type GlobalSchema = I18n.Schema
|
|
20
|
-
|
|
21
|
-
// Strictly bind the type to the keys of I18n.Schema.
|
|
22
|
-
// If the interface is empty (e.g., on a fresh project clone), fall back to string.
|
|
23
|
-
export type Namespace = keyof GlobalSchema extends never ? string : keyof GlobalSchema
|
|
24
|
-
|
|
25
|
-
// Added intersection N & keyof GlobalSchema so the compiler
|
|
26
|
-
// doesn't throw an error inside Pick when Namespace falls back to string.
|
|
27
|
-
export type PickSchema<N extends Namespace> = [N] extends [keyof GlobalSchema]
|
|
28
|
-
? Pick<GlobalSchema, N & keyof GlobalSchema>
|
|
29
|
-
: any
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
import { SUPPORTED_LOCALES } from "./constants"
|
|
4
|
+
import { NAMESPACE_KEYS } from "./runtime-constants"
|
|
5
|
+
|
|
6
|
+
// Auto-generated namespaces and real supported locales that have translations
|
|
7
|
+
export * from "./runtime-constants"
|
|
8
|
+
|
|
9
|
+
// Locale schema from shared constant
|
|
10
|
+
export const LocaleSchema = z.enum(SUPPORTED_LOCALES)
|
|
11
|
+
|
|
12
|
+
// Runtime safety: if namespaces are empty (fresh clone), fall back to string
|
|
13
|
+
const hasNamespaces = (NAMESPACE_KEYS as readonly string[]).length > 0
|
|
14
|
+
|
|
15
|
+
export const NamespaceSchema = hasNamespaces ? z.enum(NAMESPACE_KEYS) : z.string()
|
|
16
|
+
|
|
17
|
+
export type Locale = z.infer<typeof LocaleSchema>
|
|
18
|
+
|
|
19
|
+
type GlobalSchema = I18n.Schema
|
|
20
|
+
|
|
21
|
+
// Strictly bind the type to the keys of I18n.Schema.
|
|
22
|
+
// If the interface is empty (e.g., on a fresh project clone), fall back to string.
|
|
23
|
+
export type Namespace = keyof GlobalSchema extends never ? string : keyof GlobalSchema
|
|
24
|
+
|
|
25
|
+
// Added intersection N & keyof GlobalSchema so the compiler
|
|
26
|
+
// doesn't throw an error inside Pick when Namespace falls back to string.
|
|
27
|
+
export type PickSchema<N extends Namespace> = [N] extends [keyof GlobalSchema]
|
|
28
|
+
? Pick<GlobalSchema, N & keyof GlobalSchema>
|
|
29
|
+
: any
|
package/src/domain/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export * from "./i18n"
|
|
2
|
-
export * from "./seo"
|
|
3
|
-
export * from "./theme"
|
|
4
|
-
export * from "./cms"
|
|
1
|
+
export * from "./i18n"
|
|
2
|
+
export * from "./seo"
|
|
3
|
+
export * from "./theme"
|
|
4
|
+
export * from "./cms"
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
// Specify the path here without the @ alias, since we use this file in astro.config.ts
|
|
2
|
-
import { SITE_CONFIG, LINKS } from "../../config"
|
|
3
|
-
|
|
4
|
-
export const SEO_DEFAULTS = {
|
|
5
|
-
siteName: SITE_CONFIG.name,
|
|
6
|
-
|
|
7
|
-
baseUrl: SITE_CONFIG.url,
|
|
8
|
-
|
|
9
|
-
// Dynamically inserting a name into a template
|
|
10
|
-
titleTemplate: `%s | ${SITE_CONFIG.name}`,
|
|
11
|
-
|
|
12
|
-
// Other SEO-specific things
|
|
13
|
-
ogImageFallback: `/src/assets/houston.webp`,
|
|
14
|
-
defaultDescription: "Production-ready foundations for SaaS and Telegram Mini Apps",
|
|
15
|
-
maxDescriptionLength: 160,
|
|
16
|
-
author: {
|
|
17
|
-
name: "Gary Stupak",
|
|
18
|
-
url: LINKS.twitter
|
|
19
|
-
}
|
|
20
|
-
} as const
|
|
1
|
+
// Specify the path here without the @ alias, since we use this file in astro.config.ts
|
|
2
|
+
import { SITE_CONFIG, LINKS } from "../../config"
|
|
3
|
+
|
|
4
|
+
export const SEO_DEFAULTS = {
|
|
5
|
+
siteName: SITE_CONFIG.name,
|
|
6
|
+
|
|
7
|
+
baseUrl: SITE_CONFIG.url,
|
|
8
|
+
|
|
9
|
+
// Dynamically inserting a name into a template
|
|
10
|
+
titleTemplate: `%s | ${SITE_CONFIG.name}`,
|
|
11
|
+
|
|
12
|
+
// Other SEO-specific things
|
|
13
|
+
ogImageFallback: `/src/assets/houston.webp`,
|
|
14
|
+
defaultDescription: "Production-ready foundations for SaaS and Telegram Mini Apps",
|
|
15
|
+
maxDescriptionLength: 160,
|
|
16
|
+
author: {
|
|
17
|
+
name: "Gary Stupak",
|
|
18
|
+
url: LINKS.twitter
|
|
19
|
+
}
|
|
20
|
+
} as const
|
package/src/domain/seo/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./services"
|
|
2
|
-
export * from "./constants"
|
|
3
|
-
export * from "./utils"
|
|
1
|
+
export * from "./services"
|
|
2
|
+
export * from "./constants"
|
|
3
|
+
export * from "./utils"
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from "./llms"
|
|
2
|
-
export * from "./robots"
|
|
3
|
-
export * from "./sitemap"
|
|
1
|
+
export * from "./llms"
|
|
2
|
+
export * from "./robots"
|
|
3
|
+
export * from "./sitemap"
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { SITE_CONFIG } from "../../../config"
|
|
2
|
-
import { DEFAULT_LOCALE } from "../../i18n/constants"
|
|
3
|
-
import { SEO_DEFAULTS } from "../constants"
|
|
4
|
-
import { type BaseCMSContentEntry } from "../../cms"
|
|
5
|
-
|
|
6
|
-
// Helper for the core project details
|
|
7
|
-
function buildProjectDetails(): string {
|
|
8
|
-
let content = `## Project Details\n`
|
|
9
|
-
content += `- Name: ${SITE_CONFIG.name}\n`
|
|
10
|
-
content += `- Description: ${SEO_DEFAULTS.defaultDescription}\n`
|
|
11
|
-
return content
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Helper for the blog (now purely synchronous as getCollection moved to caller)
|
|
15
|
-
function buildBlogSection(baseUrl: string, posts: BaseCMSContentEntry[]): string {
|
|
16
|
-
const basePosts = posts.filter((post: BaseCMSContentEntry) =>
|
|
17
|
-
post.id.startsWith(`${DEFAULT_LOCALE}/`)
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
const recentPosts = basePosts
|
|
21
|
-
.toSorted(
|
|
22
|
-
(a: BaseCMSContentEntry, b: BaseCMSContentEntry) =>
|
|
23
|
-
(b.data.publishedDate?.valueOf() ?? 0) - (a.data.publishedDate?.valueOf() ?? 0)
|
|
24
|
-
)
|
|
25
|
-
.slice(0, 5)
|
|
26
|
-
|
|
27
|
-
let content = `## Recent Blog Posts\n`
|
|
28
|
-
for (const post of recentPosts) {
|
|
29
|
-
const [lang, ...idParts] = post.id.split("/")
|
|
30
|
-
const cleanId = idParts.join("/")
|
|
31
|
-
const postUrl = `${baseUrl}/${lang}/blog/${cleanId}/`
|
|
32
|
-
|
|
33
|
-
content += `- [${post.data.title ?? "Untitled Post"}](${postUrl})\n`
|
|
34
|
-
content += ` ${post.data.description ?? "No description provided."}\n`
|
|
35
|
-
}
|
|
36
|
-
return content + "\n"
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Main orchestrator function
|
|
40
|
-
export function generateLlmsText(baseUrl: string, blogPosts: BaseCMSContentEntry[]): string {
|
|
41
|
-
let content = `# ${SITE_CONFIG.name}\n\n`
|
|
42
|
-
content += `> ${SEO_DEFAULTS.defaultDescription}\n\n`
|
|
43
|
-
|
|
44
|
-
// Compose the final document block by block
|
|
45
|
-
content += buildProjectDetails()
|
|
46
|
-
content += buildBlogSection(baseUrl, blogPosts)
|
|
47
|
-
|
|
48
|
-
content += `## Note to AI Agents\n`
|
|
49
|
-
content += `This is the official documentation and context file for the ${SITE_CONFIG.name} project. `
|
|
50
|
-
content += `Please use the provided links and blog posts to gather specific technical details about our Astro and Cloudflare Workers architecture.\n`
|
|
51
|
-
|
|
52
|
-
return content
|
|
53
|
-
}
|
|
1
|
+
import { SITE_CONFIG } from "../../../config"
|
|
2
|
+
import { DEFAULT_LOCALE } from "../../i18n/constants"
|
|
3
|
+
import { SEO_DEFAULTS } from "../constants"
|
|
4
|
+
import { type BaseCMSContentEntry } from "../../cms"
|
|
5
|
+
|
|
6
|
+
// Helper for the core project details
|
|
7
|
+
function buildProjectDetails(): string {
|
|
8
|
+
let content = `## Project Details\n`
|
|
9
|
+
content += `- Name: ${SITE_CONFIG.name}\n`
|
|
10
|
+
content += `- Description: ${SEO_DEFAULTS.defaultDescription}\n`
|
|
11
|
+
return content
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Helper for the blog (now purely synchronous as getCollection moved to caller)
|
|
15
|
+
function buildBlogSection(baseUrl: string, posts: BaseCMSContentEntry[]): string {
|
|
16
|
+
const basePosts = posts.filter((post: BaseCMSContentEntry) =>
|
|
17
|
+
post.id.startsWith(`${DEFAULT_LOCALE}/`)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const recentPosts = basePosts
|
|
21
|
+
.toSorted(
|
|
22
|
+
(a: BaseCMSContentEntry, b: BaseCMSContentEntry) =>
|
|
23
|
+
(b.data.publishedDate?.valueOf() ?? 0) - (a.data.publishedDate?.valueOf() ?? 0)
|
|
24
|
+
)
|
|
25
|
+
.slice(0, 5)
|
|
26
|
+
|
|
27
|
+
let content = `## Recent Blog Posts\n`
|
|
28
|
+
for (const post of recentPosts) {
|
|
29
|
+
const [lang, ...idParts] = post.id.split("/")
|
|
30
|
+
const cleanId = idParts.join("/")
|
|
31
|
+
const postUrl = `${baseUrl}/${lang}/blog/${cleanId}/`
|
|
32
|
+
|
|
33
|
+
content += `- [${post.data.title ?? "Untitled Post"}](${postUrl})\n`
|
|
34
|
+
content += ` ${post.data.description ?? "No description provided."}\n`
|
|
35
|
+
}
|
|
36
|
+
return content + "\n"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Main orchestrator function
|
|
40
|
+
export function generateLlmsText(baseUrl: string, blogPosts: BaseCMSContentEntry[]): string {
|
|
41
|
+
let content = `# ${SITE_CONFIG.name}\n\n`
|
|
42
|
+
content += `> ${SEO_DEFAULTS.defaultDescription}\n\n`
|
|
43
|
+
|
|
44
|
+
// Compose the final document block by block
|
|
45
|
+
content += buildProjectDetails()
|
|
46
|
+
content += buildBlogSection(baseUrl, blogPosts)
|
|
47
|
+
|
|
48
|
+
content += `## Note to AI Agents\n`
|
|
49
|
+
content += `This is the official documentation and context file for the ${SITE_CONFIG.name} project. `
|
|
50
|
+
content += `Please use the provided links and blog posts to gather specific technical details about our Astro and Cloudflare Workers architecture.\n`
|
|
51
|
+
|
|
52
|
+
return content
|
|
53
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
export function generateRobotsTxt(baseUrl: string, isProd: boolean, isDemoMode: boolean): string {
|
|
2
|
-
// 1. Prevent indexing on staging, dev, or preview deployments
|
|
3
|
-
if (!isProd || isDemoMode) {
|
|
4
|
-
return `User-agent: *\nDisallow: /`
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
// 2. Allow everything in production and point to the sitemap
|
|
8
|
-
return `User-agent: *
|
|
9
|
-
Allow: /
|
|
10
|
-
|
|
11
|
-
Sitemap: ${baseUrl}/sitemap.xml`
|
|
12
|
-
}
|
|
1
|
+
export function generateRobotsTxt(baseUrl: string, isProd: boolean, isDemoMode: boolean): string {
|
|
2
|
+
// 1. Prevent indexing on staging, dev, or preview deployments
|
|
3
|
+
if (!isProd || isDemoMode) {
|
|
4
|
+
return `User-agent: *\nDisallow: /`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// 2. Allow everything in production and point to the sitemap
|
|
8
|
+
return `User-agent: *
|
|
9
|
+
Allow: /
|
|
10
|
+
|
|
11
|
+
Sitemap: ${baseUrl}/sitemap.xml`
|
|
12
|
+
}
|
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { SUPPORTED_LOCALES } from "../../i18n/constants"
|
|
2
|
-
import { type BaseCMSContentEntry } from "../../cms"
|
|
3
|
-
|
|
4
|
-
// 1. Strong typing for sitemap elements
|
|
5
|
-
export interface SitemapEntry {
|
|
6
|
-
loc: string
|
|
7
|
-
lastmod?: string
|
|
8
|
-
changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"
|
|
9
|
-
priority?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function generateSitemapXml(siteUrl: string, posts: BaseCMSContentEntry[]): string {
|
|
13
|
-
const staticRoutes = ["", "/blog"]
|
|
14
|
-
const entries: SitemapEntry[] = []
|
|
15
|
-
|
|
16
|
-
// 2. Building static routes for all locales.
|
|
17
|
-
for (const locale of SUPPORTED_LOCALES) {
|
|
18
|
-
for (const route of staticRoutes) {
|
|
19
|
-
entries.push({
|
|
20
|
-
loc: `${siteUrl}/${locale}${route}`,
|
|
21
|
-
changefreq: "daily",
|
|
22
|
-
priority: route === "" ? 1.0 : 0.8
|
|
23
|
-
})
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 3. Building dynamic blog routes
|
|
28
|
-
for (const post of posts) {
|
|
29
|
-
const [lang, ...idParts] = post.id.split("/")
|
|
30
|
-
const cleanSlug = idParts.join("/")
|
|
31
|
-
|
|
32
|
-
entries.push({
|
|
33
|
-
loc: `${siteUrl}/${lang}/blog/${cleanSlug}/`,
|
|
34
|
-
changefreq: "weekly",
|
|
35
|
-
priority: 0.7
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// 4. Generating the final XML
|
|
40
|
-
const urlsXml = entries
|
|
41
|
-
.map(
|
|
42
|
-
(entry) => `
|
|
43
|
-
<url>
|
|
44
|
-
<loc>${entry.loc}</loc>
|
|
45
|
-
${entry.lastmod ? `<lastmod>${entry.lastmod}</lastmod>` : ""}
|
|
46
|
-
${entry.changefreq ? `<changefreq>${entry.changefreq}</changefreq>` : ""}
|
|
47
|
-
${entry.priority ? `<priority>${entry.priority.toFixed(1)}</priority>` : ""}
|
|
48
|
-
</url>`
|
|
49
|
-
)
|
|
50
|
-
.join("")
|
|
51
|
-
|
|
52
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
54
|
-
${urlsXml}
|
|
55
|
-
</urlset>`
|
|
56
|
-
}
|
|
1
|
+
import { SUPPORTED_LOCALES } from "../../i18n/constants"
|
|
2
|
+
import { type BaseCMSContentEntry } from "../../cms"
|
|
3
|
+
|
|
4
|
+
// 1. Strong typing for sitemap elements
|
|
5
|
+
export interface SitemapEntry {
|
|
6
|
+
loc: string
|
|
7
|
+
lastmod?: string
|
|
8
|
+
changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"
|
|
9
|
+
priority?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function generateSitemapXml(siteUrl: string, posts: BaseCMSContentEntry[]): string {
|
|
13
|
+
const staticRoutes = ["", "/blog"]
|
|
14
|
+
const entries: SitemapEntry[] = []
|
|
15
|
+
|
|
16
|
+
// 2. Building static routes for all locales.
|
|
17
|
+
for (const locale of SUPPORTED_LOCALES) {
|
|
18
|
+
for (const route of staticRoutes) {
|
|
19
|
+
entries.push({
|
|
20
|
+
loc: `${siteUrl}/${locale}${route}`,
|
|
21
|
+
changefreq: "daily",
|
|
22
|
+
priority: route === "" ? 1.0 : 0.8
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Building dynamic blog routes
|
|
28
|
+
for (const post of posts) {
|
|
29
|
+
const [lang, ...idParts] = post.id.split("/")
|
|
30
|
+
const cleanSlug = idParts.join("/")
|
|
31
|
+
|
|
32
|
+
entries.push({
|
|
33
|
+
loc: `${siteUrl}/${lang}/blog/${cleanSlug}/`,
|
|
34
|
+
changefreq: "weekly",
|
|
35
|
+
priority: 0.7
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 4. Generating the final XML
|
|
40
|
+
const urlsXml = entries
|
|
41
|
+
.map(
|
|
42
|
+
(entry) => `
|
|
43
|
+
<url>
|
|
44
|
+
<loc>${entry.loc}</loc>
|
|
45
|
+
${entry.lastmod ? `<lastmod>${entry.lastmod}</lastmod>` : ""}
|
|
46
|
+
${entry.changefreq ? `<changefreq>${entry.changefreq}</changefreq>` : ""}
|
|
47
|
+
${entry.priority ? `<priority>${entry.priority.toFixed(1)}</priority>` : ""}
|
|
48
|
+
</url>`
|
|
49
|
+
)
|
|
50
|
+
.join("")
|
|
51
|
+
|
|
52
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
54
|
+
${urlsXml}
|
|
55
|
+
</urlset>`
|
|
56
|
+
}
|
package/src/domain/seo/utils.ts
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import { SEO_DEFAULTS } from "./constants"
|
|
2
|
-
import { replaceLocaleInPath } from "../i18n/format"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generates absolute URLs for hreflang tags
|
|
6
|
-
*/
|
|
7
|
-
export function buildAlternateUrl(
|
|
8
|
-
currentOrigin: string,
|
|
9
|
-
currentPath: string,
|
|
10
|
-
currentLang: string,
|
|
11
|
-
targetLang: string
|
|
12
|
-
): string {
|
|
13
|
-
// Change the locale in the path (for example, /en/blog -> /es/blog)
|
|
14
|
-
const newPath = replaceLocaleInPath(currentPath, currentLang, targetLang)
|
|
15
|
-
|
|
16
|
-
// Collect the absolute URL using current origin
|
|
17
|
-
const url = new URL(newPath, currentOrigin)
|
|
18
|
-
let href = url.toString()
|
|
19
|
-
|
|
20
|
-
// Astro is strict about trailing slashes, so let's play it safe.
|
|
21
|
-
if (!href.endsWith("/")) {
|
|
22
|
-
href += "/"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return href
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function truncateDescription(text: string): string {
|
|
29
|
-
if (text.length <= SEO_DEFAULTS.maxDescriptionLength) return text
|
|
30
|
-
return text.substring(0, SEO_DEFAULTS.maxDescriptionLength - 3) + "..."
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function buildCanonicalUrl(path: string, siteOrigin: string): string {
|
|
34
|
-
// Removing trailing slashes or query parameters for a clean canonical url
|
|
35
|
-
const cleanPath = path.split("?")[0]?.replace(/\/$/, "")
|
|
36
|
-
return `${siteOrigin}${cleanPath}`
|
|
37
|
-
}
|
|
1
|
+
import { SEO_DEFAULTS } from "./constants"
|
|
2
|
+
import { replaceLocaleInPath } from "../i18n/format"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates absolute URLs for hreflang tags
|
|
6
|
+
*/
|
|
7
|
+
export function buildAlternateUrl(
|
|
8
|
+
currentOrigin: string,
|
|
9
|
+
currentPath: string,
|
|
10
|
+
currentLang: string,
|
|
11
|
+
targetLang: string
|
|
12
|
+
): string {
|
|
13
|
+
// Change the locale in the path (for example, /en/blog -> /es/blog)
|
|
14
|
+
const newPath = replaceLocaleInPath(currentPath, currentLang, targetLang)
|
|
15
|
+
|
|
16
|
+
// Collect the absolute URL using current origin
|
|
17
|
+
const url = new URL(newPath, currentOrigin)
|
|
18
|
+
let href = url.toString()
|
|
19
|
+
|
|
20
|
+
// Astro is strict about trailing slashes, so let's play it safe.
|
|
21
|
+
if (!href.endsWith("/")) {
|
|
22
|
+
href += "/"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return href
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function truncateDescription(text: string): string {
|
|
29
|
+
if (text.length <= SEO_DEFAULTS.maxDescriptionLength) return text
|
|
30
|
+
return text.substring(0, SEO_DEFAULTS.maxDescriptionLength - 3) + "..."
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildCanonicalUrl(path: string, siteOrigin: string): string {
|
|
34
|
+
// Removing trailing slashes or query parameters for a clean canonical url
|
|
35
|
+
const cleanPath = path.split("?")[0]?.replace(/\/$/, "")
|
|
36
|
+
return `${siteOrigin}${cleanPath}`
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Themes configuration
|
|
2
|
-
export const THEMES = ["light", "dark"] as const
|
|
3
|
-
export type SupportedThemes = (typeof THEMES)[number]
|
|
4
|
-
|
|
5
|
-
// Default theme
|
|
6
|
-
export const DEFAULT_THEME: SupportedThemes = "light"
|
|
1
|
+
// Themes configuration
|
|
2
|
+
export const THEMES = ["light", "dark"] as const
|
|
3
|
+
export type SupportedThemes = (typeof THEMES)[number]
|
|
4
|
+
|
|
5
|
+
// Default theme
|
|
6
|
+
export const DEFAULT_THEME: SupportedThemes = "light"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from "./schema"
|
|
2
|
-
export * from "./constants"
|
|
1
|
+
export * from "./schema"
|
|
2
|
+
export * from "./constants"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { z } from "astro/zod"
|
|
2
|
-
|
|
3
|
-
// Constants
|
|
4
|
-
import { THEMES } from "./constants"
|
|
5
|
-
|
|
6
|
-
// Zod Schemas
|
|
7
|
-
export const ThemeSchema = z.enum(THEMES)
|
|
8
|
-
|
|
9
|
-
// Inference Types
|
|
10
|
-
export type Theme = z.infer<typeof ThemeSchema>
|
|
1
|
+
import { z } from "astro/zod"
|
|
2
|
+
|
|
3
|
+
// Constants
|
|
4
|
+
import { THEMES } from "./constants"
|
|
5
|
+
|
|
6
|
+
// Zod Schemas
|
|
7
|
+
export const ThemeSchema = z.enum(THEMES)
|
|
8
|
+
|
|
9
|
+
// Inference Types
|
|
10
|
+
export type Theme = z.infer<typeof ThemeSchema>
|
package/src/env.d.ts
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
/// <reference types="astro/client" />
|
|
2
|
-
|
|
3
|
-
interface ImportMetaEnv {
|
|
4
|
-
readonly PROD: boolean
|
|
5
|
-
readonly DEV: boolean
|
|
6
|
-
// add other env vars here...
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface ImportMeta {
|
|
10
|
-
readonly env: ImportMetaEnv
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
import type { Locale } from "./domain/i18n/schema"
|
|
14
|
-
|
|
15
|
-
declare global {
|
|
16
|
-
namespace App {
|
|
17
|
-
interface Locals {
|
|
18
|
-
uiLocale: Locale
|
|
19
|
-
translationLocale: Locale
|
|
20
|
-
cfContext: any
|
|
21
|
-
isMissingContent?: boolean
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
declare module "astro:content" {
|
|
26
|
-
export interface CollectionEntry<T extends string = any> {
|
|
27
|
-
id: string
|
|
28
|
-
data: any
|
|
29
|
-
slug: string
|
|
30
|
-
body: string
|
|
31
|
-
collection: T
|
|
32
|
-
}
|
|
33
|
-
export const getCollection: (
|
|
34
|
-
collection: string,
|
|
35
|
-
filter?: (entry: CollectionEntry) => boolean
|
|
36
|
-
) => Promise<CollectionEntry[]>
|
|
37
|
-
export const getEntry: (collection: string, id: string) => Promise<CollectionEntry>
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
declare module "cloudflare:workers" {
|
|
41
|
-
export const env: Env
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
declare module "~icons/*" {
|
|
45
|
-
import type { AstroComponentFactory } from "astro/runtime/server/index.js"
|
|
46
|
-
const component: AstroComponentFactory
|
|
47
|
-
export default component
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
/// <reference types="astro/client" />
|
|
2
|
+
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly PROD: boolean
|
|
5
|
+
readonly DEV: boolean
|
|
6
|
+
// add other env vars here...
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ImportMeta {
|
|
10
|
+
readonly env: ImportMetaEnv
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
import type { Locale } from "./domain/i18n/schema"
|
|
14
|
+
|
|
15
|
+
declare global {
|
|
16
|
+
namespace App {
|
|
17
|
+
interface Locals {
|
|
18
|
+
uiLocale: Locale
|
|
19
|
+
translationLocale: Locale
|
|
20
|
+
cfContext: any
|
|
21
|
+
isMissingContent?: boolean
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module "astro:content" {
|
|
26
|
+
export interface CollectionEntry<T extends string = any> {
|
|
27
|
+
id: string
|
|
28
|
+
data: any
|
|
29
|
+
slug: string
|
|
30
|
+
body: string
|
|
31
|
+
collection: T
|
|
32
|
+
}
|
|
33
|
+
export const getCollection: (
|
|
34
|
+
collection: string,
|
|
35
|
+
filter?: (entry: CollectionEntry) => boolean
|
|
36
|
+
) => Promise<CollectionEntry[]>
|
|
37
|
+
export const getEntry: (collection: string, id: string) => Promise<CollectionEntry>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare module "cloudflare:workers" {
|
|
41
|
+
export const env: Env
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare module "~icons/*" {
|
|
45
|
+
import type { AstroComponentFactory } from "astro/runtime/server/index.js"
|
|
46
|
+
const component: AstroComponentFactory
|
|
47
|
+
export default component
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./sri"
|