@rimelight/ui 0.0.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/LICENSE +21 -0
- package/README.md +30 -0
- package/i18n-data.json +186 -0
- package/package.json +49 -0
- package/src/components/FormattedDate.astro +22 -0
- package/src/components/HeaderLink.astro +24 -0
- package/src/components/HttpObservatory.astro +103 -0
- package/src/components/LighthouseScores.astro +204 -0
- package/src/components/MissingTranslationBanner.astro +39 -0
- package/src/components/TableOfContents.astro +106 -0
- package/src/components/ThemeToggle.astro +71 -0
- package/src/components/accordion/Accordion.astro +257 -0
- package/src/components/accordion/AccordionContent.astro +34 -0
- package/src/components/accordion/AccordionItem.astro +29 -0
- package/src/components/accordion/AccordionTrigger.astro +33 -0
- package/src/components/alert/Alert.astro +34 -0
- package/src/components/alert/AlertDescription.astro +15 -0
- package/src/components/alert/AlertTitle.astro +17 -0
- package/src/components/alert-dialog/AlertDialog.astro +275 -0
- package/src/components/alert-dialog/AlertDialogAction.astro +46 -0
- package/src/components/alert-dialog/AlertDialogCancel.astro +46 -0
- package/src/components/alert-dialog/AlertDialogContent.astro +52 -0
- package/src/components/alert-dialog/AlertDialogDescription.astro +18 -0
- package/src/components/alert-dialog/AlertDialogFooter.astro +16 -0
- package/src/components/alert-dialog/AlertDialogHeader.astro +14 -0
- package/src/components/alert-dialog/AlertDialogTitle.astro +20 -0
- package/src/components/alert-dialog/AlertDialogTrigger.astro +47 -0
- package/src/components/aspect-ratio/AspectRatio.astro +32 -0
- package/src/components/avatar/Avatar.astro +29 -0
- package/src/components/avatar/AvatarFallback.astro +18 -0
- package/src/components/avatar/AvatarImage.astro +66 -0
- package/src/components/badge/Badge.astro +57 -0
- package/src/components/breadcrumb/Breadcrumb.astro +12 -0
- package/src/components/breadcrumb/BreadcrumbEllipsis.astro +29 -0
- package/src/components/breadcrumb/BreadcrumbItem.astro +15 -0
- package/src/components/breadcrumb/BreadcrumbLink.astro +24 -0
- package/src/components/breadcrumb/BreadcrumbList.astro +17 -0
- package/src/components/breadcrumb/BreadcrumbPage.astro +22 -0
- package/src/components/breadcrumb/BreadcrumbSeparator.astro +24 -0
- package/src/components/button/Button.astro +58 -0
- package/src/components/button-group/ButtonGroup.astro +64 -0
- package/src/components/button-group/ButtonGroupSeparator.astro +20 -0
- package/src/components/button-group/ButtonGroupText.astro +19 -0
- package/src/components/card/Card.astro +36 -0
- package/src/components/card/CardAction.astro +16 -0
- package/src/components/card/CardContent.astro +16 -0
- package/src/components/card/CardDescription.astro +16 -0
- package/src/components/card/CardFooter.astro +16 -0
- package/src/components/card/CardHeader.astro +19 -0
- package/src/components/card/CardTitle.astro +16 -0
- package/src/components/carousel/Carousel.astro +55 -0
- package/src/components/carousel/CarouselContent.astro +26 -0
- package/src/components/carousel/CarouselItem.astro +26 -0
- package/src/components/carousel/CarouselNext.astro +39 -0
- package/src/components/carousel/CarouselPrevious.astro +41 -0
- package/src/components/carousel/carousel-script.ts +188 -0
- package/src/components/carousel/index.ts +1 -0
- package/src/components/checkbox/Checkbox.astro +133 -0
- package/src/components/collapsible/Collapsible.astro +161 -0
- package/src/components/collapsible/CollapsibleContent.astro +22 -0
- package/src/components/collapsible/CollapsibleTrigger.astro +44 -0
- package/src/components/dialog/Dialog.astro +389 -0
- package/src/components/dialog/DialogClose.astro +35 -0
- package/src/components/dialog/DialogContent.astro +78 -0
- package/src/components/dialog/DialogDescription.astro +14 -0
- package/src/components/dialog/DialogFooter.astro +14 -0
- package/src/components/dialog/DialogHeader.astro +14 -0
- package/src/components/dialog/DialogTitle.astro +18 -0
- package/src/components/dialog/DialogTrigger.astro +47 -0
- package/src/components/dropdown/Dropdown.astro +604 -0
- package/src/components/dropdown/DropdownContent.astro +118 -0
- package/src/components/dropdown/DropdownItem.astro +48 -0
- package/src/components/dropdown/DropdownLabel.astro +29 -0
- package/src/components/dropdown/DropdownSeparator.astro +21 -0
- package/src/components/dropdown/DropdownShortcut.astro +17 -0
- package/src/components/dropdown/DropdownSub.astro +15 -0
- package/src/components/dropdown/DropdownSubContent.astro +39 -0
- package/src/components/dropdown/DropdownSubTrigger.astro +34 -0
- package/src/components/dropdown/DropdownTrigger.astro +52 -0
- package/src/components/dropzone/Dropzone.astro +236 -0
- package/src/components/dropzone/DropzoneFilesList.astro +26 -0
- package/src/components/dropzone/DropzoneLoadingIndicator.astro +10 -0
- package/src/components/dropzone/DropzoneUploadIndicator.astro +10 -0
- package/src/components/embeds/YoutubeEmbed.astro +94 -0
- package/src/components/hero/Hero.astro +166 -0
- package/src/components/icons/Flag.astro +62 -0
- package/src/components/image/ExtendedImage.astro +46 -0
- package/src/components/image/Image.astro +25 -0
- package/src/components/input/Input.astro +27 -0
- package/src/components/input-group/InputGroup.astro +28 -0
- package/src/components/input-group/InputGroupAddon.astro +76 -0
- package/src/components/input-group/InputGroupButton.astro +36 -0
- package/src/components/input-group/InputGroupInput.astro +18 -0
- package/src/components/input-group/InputGroupText.astro +17 -0
- package/src/components/input-group/InputGroupTextarea.astro +22 -0
- package/src/components/input-otp/InputOtp.astro +352 -0
- package/src/components/input-otp/InputOtpGroup.astro +16 -0
- package/src/components/input-otp/InputOtpSeparator.astro +25 -0
- package/src/components/input-otp/InputOtpSlot.astro +50 -0
- package/src/components/input-otp/InputOtpTypes.ts +6 -0
- package/src/components/item/Item.astro +52 -0
- package/src/components/item/ItemActions.astro +16 -0
- package/src/components/item/ItemContent.astro +16 -0
- package/src/components/item/ItemDescription.astro +19 -0
- package/src/components/item/ItemFooter.astro +16 -0
- package/src/components/item/ItemGroup.astro +16 -0
- package/src/components/item/ItemHeader.astro +16 -0
- package/src/components/item/ItemMedia.astro +40 -0
- package/src/components/item/ItemSeparator.astro +23 -0
- package/src/components/item/ItemTitle.astro +16 -0
- package/src/components/kbd/Kbd.astro +21 -0
- package/src/components/kbd/KbdGroup.astro +16 -0
- package/src/components/label/Label.astro +24 -0
- package/src/components/marquee/Marquee.astro +73 -0
- package/src/components/native-select/NativeSelect.astro +68 -0
- package/src/components/native-select/NativeSelectOptGroup.astro +15 -0
- package/src/components/native-select/NativeSelectOption.astro +15 -0
- package/src/components/pagination/Pagination.astro +20 -0
- package/src/components/pagination/PaginationContent.astro +16 -0
- package/src/components/pagination/PaginationEllipsis.astro +37 -0
- package/src/components/pagination/PaginationItem.astro +16 -0
- package/src/components/pagination/PaginationLink.astro +27 -0
- package/src/components/pagination/PaginationNext.astro +33 -0
- package/src/components/pagination/PaginationPrevious.astro +33 -0
- package/src/components/popover/Popover.astro +717 -0
- package/src/components/popover/PopoverContent.astro +102 -0
- package/src/components/popover/PopoverDescription.astro +14 -0
- package/src/components/popover/PopoverHeader.astro +14 -0
- package/src/components/popover/PopoverTitle.astro +14 -0
- package/src/components/popover/PopoverTrigger.astro +51 -0
- package/src/components/progress/Progress.astro +154 -0
- package/src/components/prose/Prose.astro +619 -0
- package/src/components/radio-group/RadioGroup.astro +162 -0
- package/src/components/radio-group/RadioGroupItem.astro +133 -0
- package/src/components/radio-group/RadioGroupTypes.ts +6 -0
- package/src/components/section/Section.astro +167 -0
- package/src/components/select/Select.astro +752 -0
- package/src/components/select/SelectContent.astro +94 -0
- package/src/components/select/SelectGroup.astro +9 -0
- package/src/components/select/SelectItem.astro +51 -0
- package/src/components/select/SelectLabel.astro +14 -0
- package/src/components/select/SelectSearch.astro +49 -0
- package/src/components/select/SelectSeparator.astro +12 -0
- package/src/components/select/SelectTrigger.astro +56 -0
- package/src/components/select/SelectTypes.ts +13 -0
- package/src/components/select/SelectValue.astro +19 -0
- package/src/components/separator/Separator.astro +36 -0
- package/src/components/sheet/Sheet.astro +15 -0
- package/src/components/sheet/SheetClose.astro +15 -0
- package/src/components/sheet/SheetContent.astro +89 -0
- package/src/components/sheet/SheetDescription.astro +16 -0
- package/src/components/sheet/SheetFooter.astro +16 -0
- package/src/components/sheet/SheetHeader.astro +16 -0
- package/src/components/sheet/SheetTitle.astro +16 -0
- package/src/components/sheet/SheetTrigger.astro +15 -0
- package/src/components/sidebar/Sidebar.astro +231 -0
- package/src/components/sidebar/SidebarContent.astro +25 -0
- package/src/components/sidebar/SidebarFooter.astro +22 -0
- package/src/components/sidebar/SidebarGroup.astro +22 -0
- package/src/components/sidebar/SidebarGroupContent.astro +22 -0
- package/src/components/sidebar/SidebarGroupLabel.astro +53 -0
- package/src/components/sidebar/SidebarHeader.astro +22 -0
- package/src/components/sidebar/SidebarInput.astro +24 -0
- package/src/components/sidebar/SidebarInset.astro +22 -0
- package/src/components/sidebar/SidebarMenu.astro +22 -0
- package/src/components/sidebar/SidebarMenuAction.astro +63 -0
- package/src/components/sidebar/SidebarMenuBadge.astro +31 -0
- package/src/components/sidebar/SidebarMenuButton.astro +124 -0
- package/src/components/sidebar/SidebarMenuItem.astro +22 -0
- package/src/components/sidebar/SidebarMenuSkeleton.astro +43 -0
- package/src/components/sidebar/SidebarMenuSub.astro +25 -0
- package/src/components/sidebar/SidebarMenuSubButton.astro +52 -0
- package/src/components/sidebar/SidebarMenuSubItem.astro +17 -0
- package/src/components/sidebar/SidebarProvider.astro +215 -0
- package/src/components/sidebar/SidebarRail.astro +72 -0
- package/src/components/sidebar/SidebarSeparator.astro +25 -0
- package/src/components/sidebar/SidebarTrigger.astro +69 -0
- package/src/components/skeleton/Skeleton.astro +14 -0
- package/src/components/slider/Slider.astro +425 -0
- package/src/components/spinner/Spinner.astro +23 -0
- package/src/components/switch/Switch.astro +192 -0
- package/src/components/switch/SwitchTypes.ts +6 -0
- package/src/components/table/Table.astro +20 -0
- package/src/components/table/TableBody.astro +18 -0
- package/src/components/table/TableCaption.astro +18 -0
- package/src/components/table/TableCell.astro +18 -0
- package/src/components/table/TableFoot.astro +18 -0
- package/src/components/table/TableHead.astro +18 -0
- package/src/components/table/TableHeader.astro +18 -0
- package/src/components/table/TableRow.astro +17 -0
- package/src/components/tabs/Tabs.astro +270 -0
- package/src/components/tabs/TabsContent.astro +28 -0
- package/src/components/tabs/TabsList.astro +23 -0
- package/src/components/tabs/TabsTrigger.astro +34 -0
- package/src/components/textarea/Textarea.astro +31 -0
- package/src/components/toast/ToastDescription.astro +22 -0
- package/src/components/toast/ToastItem.astro +56 -0
- package/src/components/toast/ToastTemplate.astro +27 -0
- package/src/components/toast/ToastTitle.astro +59 -0
- package/src/components/toast/Toaster.astro +981 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast-manager.ts +234 -0
- package/src/components/toggle/Toggle.astro +174 -0
- package/src/components/toggle/ToggleTypes.ts +14 -0
- package/src/components/tooltip/Tooltip.astro +422 -0
- package/src/components/tooltip/TooltipContent.astro +83 -0
- package/src/components/tooltip/TooltipTrigger.astro +13 -0
- package/src/components/video/Video.astro +121 -0
- package/src/config/cookies.ts +5 -0
- package/src/config/index.ts +3 -0
- package/src/config/links.ts +4 -0
- package/src/config/site.config.ts +45 -0
- package/src/domain/cms/index.ts +1 -0
- package/src/domain/cms/types.ts +20 -0
- package/src/domain/i18n/constants.ts +21 -0
- package/src/domain/i18n/cookie-storage.ts +13 -0
- package/src/domain/i18n/country-to-locale-map.ts +67 -0
- package/src/domain/i18n/fallbacks.generated.ts +230 -0
- package/src/domain/i18n/fetcher.ts +268 -0
- package/src/domain/i18n/flags.ts +16 -0
- package/src/domain/i18n/format.ts +90 -0
- package/src/domain/i18n/index.ts +10 -0
- package/src/domain/i18n/middleware/i18n.ts +160 -0
- package/src/domain/i18n/resolve-locale.ts +55 -0
- package/src/domain/i18n/runtime-constants.ts +14 -0
- package/src/domain/i18n/schema.ts +29 -0
- package/src/domain/index.ts +4 -0
- package/src/domain/seo/constants.ts +20 -0
- package/src/domain/seo/index.ts +3 -0
- package/src/domain/seo/services/index.ts +3 -0
- package/src/domain/seo/services/llms.ts +53 -0
- package/src/domain/seo/services/robots.ts +12 -0
- package/src/domain/seo/services/sitemap.ts +56 -0
- package/src/domain/seo/utils.ts +37 -0
- package/src/domain/theme/constants.ts +6 -0
- package/src/domain/theme/index.ts +2 -0
- package/src/domain/theme/schema.ts +10 -0
- package/src/env.d.ts +49 -0
- package/src/i18n.generated.d.ts +202 -0
- package/src/lib/component-preview.ts +73 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/remark/remark-modified-time.ts +13 -0
- package/src/lib/showcase-preview-elements.ts +408 -0
- package/src/lib/showcase-renderer.ts +218 -0
- package/src/lib/showcase-utils.ts +189 -0
- package/src/lib/turnstile.ts +35 -0
- package/src/lib/utils/starwind/positioning.ts +318 -0
- package/src/locales/de/blog.json +9 -0
- package/src/locales/de/common.json +19 -0
- package/src/locales/de/errors.json +6 -0
- package/src/locales/de/home.json +20 -0
- package/src/locales/de/messages.json +8 -0
- package/src/locales/de/nav.json +13 -0
- package/src/locales/de/projects.json +9 -0
- package/src/locales/de/services.json +43 -0
- package/src/locales/en/blog.json +9 -0
- package/src/locales/en/chat.json +29 -0
- package/src/locales/en/common.json +19 -0
- package/src/locales/en/cookie_notice.json +11 -0
- package/src/locales/en/editor.json +24 -0
- package/src/locales/en/errors.json +6 -0
- package/src/locales/en/home.json +20 -0
- package/src/locales/en/messages.json +8 -0
- package/src/locales/en/nav.json +13 -0
- package/src/locales/en/projects.json +9 -0
- package/src/locales/en/services.json +43 -0
- package/src/locales/es/blog.json +9 -0
- package/src/locales/es/common.json +19 -0
- package/src/locales/es/errors.json +6 -0
- package/src/locales/es/home.json +20 -0
- package/src/locales/es/messages.json +8 -0
- package/src/locales/es/nav.json +13 -0
- package/src/locales/es/projects.json +9 -0
- package/src/locales/es/services.json +43 -0
- package/src/locales/ja/blog.json +9 -0
- package/src/locales/ja/common.json +19 -0
- package/src/locales/ja/errors.json +6 -0
- package/src/locales/ja/home.json +20 -0
- package/src/locales/ja/messages.json +8 -0
- package/src/locales/ja/nav.json +13 -0
- package/src/locales/ja/projects.json +9 -0
- package/src/locales/ja/services.json +43 -0
- package/src/locales/pt/blog.json +9 -0
- package/src/locales/pt/chat.json +29 -0
- package/src/locales/pt/common.json +19 -0
- package/src/locales/pt/cookie_notice.json +11 -0
- package/src/locales/pt/editor.json +24 -0
- package/src/locales/pt/errors.json +6 -0
- package/src/locales/pt/home.json +20 -0
- package/src/locales/pt/messages.json +8 -0
- package/src/locales/pt/nav.json +13 -0
- package/src/locales/pt/projects.json +9 -0
- package/src/locales/pt/services.json +43 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/server/cookies.ts +98 -0
- package/src/utils/server/index.ts +1 -0
- package/src/utils/shared/deep-merge.ts +36 -0
- package/src/utils/shared/embed.ts +105 -0
- package/src/utils/shared/index.ts +3 -0
- package/src/utils/shared/parse-boolean.ts +14 -0
- package/worker-configuration.d.ts +14791 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Sanitizer: Turns <script> into <script>
|
|
2
|
+
function escapeHtml(unsafe: string | number): string {
|
|
3
|
+
return String(unsafe)
|
|
4
|
+
.replace(/&/g, "&")
|
|
5
|
+
.replace(/</g, "<")
|
|
6
|
+
.replace(/>/g, ">")
|
|
7
|
+
.replace(/"/g, """)
|
|
8
|
+
.replace(/'/g, "'")
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type TemplateValues = Record<string, string | number>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interpolates {placeholders} in a template string and escapes values. - If `template` is
|
|
15
|
+
* undefined/null, returns an empty string (and logs in dev). - If `values` is undefined, returns
|
|
16
|
+
* the template unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export function fmt(template: string | null | undefined, values?: TemplateValues): string {
|
|
19
|
+
if (template == null) {
|
|
20
|
+
if (!import.meta.env.PROD) {
|
|
21
|
+
console.warn("[i18n] fmt() called with null/undefined template")
|
|
22
|
+
}
|
|
23
|
+
return ""
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!values) return template
|
|
27
|
+
|
|
28
|
+
return template.replace(/\{([^}]+)\}/g, (match, key) => {
|
|
29
|
+
const replacement = values[key]
|
|
30
|
+
|
|
31
|
+
if (replacement === undefined || replacement === null) {
|
|
32
|
+
return match
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return escapeHtml(replacement)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Simple two-form plural helper. Good for English-like languages where "1" vs "other" is enough.
|
|
41
|
+
*/
|
|
42
|
+
export function plural(count: number, singular: string, pluralForm: string): string {
|
|
43
|
+
return count === 1 ? singular : fmt(pluralForm, { count })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ICU-style plural categories used by Intl.PluralRules.
|
|
48
|
+
*/
|
|
49
|
+
export type PluralCategory = "zero" | "one" | "two" | "few" | "many" | "other"
|
|
50
|
+
|
|
51
|
+
export type PluralPatterns = Partial<Record<PluralCategory, string>>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Advanced plural helper using Intl.PluralRules + fmt().
|
|
55
|
+
*
|
|
56
|
+
* - `locale` controls plural categories (en, de, es, ja, ...).
|
|
57
|
+
* - `patterns` comes from KV (e.g. { zero, one, other }).
|
|
58
|
+
* - Missing categories fall back to `other`.
|
|
59
|
+
*
|
|
60
|
+
* Example: pluralIcu(5, 'en', { zero: 'No items', one: '{count} item', other: '{count} items' })
|
|
61
|
+
*/
|
|
62
|
+
export function pluralIcu(count: number, locale: string, patterns: PluralPatterns): string {
|
|
63
|
+
const rules = new Intl.PluralRules(locale)
|
|
64
|
+
const cat = rules.select(count) as PluralCategory
|
|
65
|
+
|
|
66
|
+
const template =
|
|
67
|
+
patterns[cat] ??
|
|
68
|
+
patterns.other ??
|
|
69
|
+
(import.meta.env.PROD ? "" : (console.warn('[i18n] pluralIcu(): missing "other" pattern'), ""))
|
|
70
|
+
|
|
71
|
+
return fmt(template, { count })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Used in the LanguageSwitcher and SeoHreflangs components
|
|
75
|
+
export function replaceLocaleInPath(
|
|
76
|
+
currentPath: string,
|
|
77
|
+
currentLocale: string,
|
|
78
|
+
newLocale: string
|
|
79
|
+
): string {
|
|
80
|
+
const prefixRegex = new RegExp(`^/${currentLocale}/`)
|
|
81
|
+
return currentPath.replace(prefixRegex, `/${newLocale}/`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Used in the LanguageSwitcher component
|
|
85
|
+
export function getLangFromUrl(pathname: string) {
|
|
86
|
+
// Remove leading/trailing slashes: '/en/about/' -> 'en/about'
|
|
87
|
+
const cleaned = pathname.replace(/^\/+|\/+$/g, "")
|
|
88
|
+
const [lang] = cleaned.split("/")
|
|
89
|
+
return lang
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./constants"
|
|
2
|
+
export * from "./runtime-constants"
|
|
3
|
+
export * from "./schema"
|
|
4
|
+
export * from "./resolve-locale"
|
|
5
|
+
export * from "./flags"
|
|
6
|
+
export * from "./format"
|
|
7
|
+
export * from "./cookie-storage"
|
|
8
|
+
export * from "./country-to-locale-map"
|
|
9
|
+
export * from "./middleware/i18n"
|
|
10
|
+
export * from "./fetcher"
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "astro"
|
|
2
|
+
import { LocaleSchema, type Locale } from "../schema"
|
|
3
|
+
import { DEFAULT_LOCALE } from "../constants"
|
|
4
|
+
import { getCookieLang, setCookieLang } from "../cookie-storage"
|
|
5
|
+
import { mapCountryToLocale } from "../country-to-locale-map"
|
|
6
|
+
import { resolveLocaleForTranslations } from "../resolve-locale"
|
|
7
|
+
|
|
8
|
+
// Regex to identify public static assets (images, fonts, scripts, etc.)
|
|
9
|
+
// These files should bypass i18n routing and locale detection.
|
|
10
|
+
const PUBLIC_FILE_REGEX =
|
|
11
|
+
/\.(?:ico|png|jpg|jpeg|svg|webp|gif|css|js|map|txt|xml|json|woff2?|avif)$/i
|
|
12
|
+
|
|
13
|
+
// List of URL prefixes that should be ignored by the i18n middleware.
|
|
14
|
+
// Includes API endpoints, internal Astro paths, and standard static routes.
|
|
15
|
+
const IGNORED_PREFIXES = ["/api", "/assets", "/_astro", "/_image", "/_actions", "/favicon"]
|
|
16
|
+
|
|
17
|
+
// Extracts the context type from Astro's MiddlewareHandler for cleaner function signatures.
|
|
18
|
+
type I18nMiddlewareContext = Parameters<MiddlewareHandler>[0]
|
|
19
|
+
|
|
20
|
+
// Determines if the current request path matches static assets or ignored prefixes.
|
|
21
|
+
// Returns true to skip i18n processing for this request.
|
|
22
|
+
function shouldBypassI18n(pathname: string): boolean {
|
|
23
|
+
if (PUBLIC_FILE_REGEX.test(pathname)) return true
|
|
24
|
+
return IGNORED_PREFIXES.some((p) => pathname.startsWith(p))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Applies strict Content Security Policy (CSP) and other security headers to the response.
|
|
28
|
+
// Wrap outgoing responses with this function to ensure consistent security posture.
|
|
29
|
+
function applySecurityHeaders(response: Response): Response {
|
|
30
|
+
// Tip: You can apply your security headers (CSP, HSTS, X-Frame-Options, etc.) here.
|
|
31
|
+
// Check the GitHub repository for the example of implementation.
|
|
32
|
+
return response
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Constructs a standardized localized URL path ensuring a trailing slash.
|
|
36
|
+
// Example: buildLocalizedPath('en', ['about', 'team']) -> '/en/about/team/'
|
|
37
|
+
function buildLocalizedPath(locale: Locale, rest: string[]): string {
|
|
38
|
+
const suffix = rest.join("/")
|
|
39
|
+
return suffix ? `/${locale}/${suffix}/` : `/${locale}/`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolves the most appropriate locale for the user based on a fallback cascade:
|
|
43
|
+
// 1. User's saved cookie preference.
|
|
44
|
+
// 2. Browser's Accept-Language header (matching 2-char code).
|
|
45
|
+
// 3. Cloudflare Geo-IP country mapping.
|
|
46
|
+
// 4. Finally, falls back to the application's DEFAULT_LOCALE.
|
|
47
|
+
function resolveFallbackLocale(context: I18nMiddlewareContext): Locale {
|
|
48
|
+
const cookieLocale = getCookieLang(context.cookies)
|
|
49
|
+
if (cookieLocale) return cookieLocale
|
|
50
|
+
|
|
51
|
+
const browserRaw = context.preferredLocale
|
|
52
|
+
if (browserRaw) {
|
|
53
|
+
// Currently we support 2-chars locales only.
|
|
54
|
+
let parsed = LocaleSchema.safeParse(browserRaw)
|
|
55
|
+
if (parsed.success) return parsed.data
|
|
56
|
+
|
|
57
|
+
// Fallback: language part only, e.g. 'pt-br' -> 'pt'
|
|
58
|
+
const short = browserRaw.split("-")[0]
|
|
59
|
+
parsed = LocaleSchema.safeParse(short)
|
|
60
|
+
if (parsed.success) return parsed.data
|
|
61
|
+
} else {
|
|
62
|
+
// Cloudflare Geo-IP Strategy
|
|
63
|
+
const country = context.request.cf?.country
|
|
64
|
+
let parsed = LocaleSchema.safeParse(mapCountryToLocale(country ?? ""))
|
|
65
|
+
if (parsed.success) return parsed.data
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return DEFAULT_LOCALE
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Main i18n middleware handler. Intercepts incoming requests, manages routing,
|
|
72
|
+
// sets locale context, and handles soft 404 protections for edge environments.
|
|
73
|
+
export const i18nMiddleware: MiddlewareHandler = async (context, next) => {
|
|
74
|
+
const url = new URL(context.request.url)
|
|
75
|
+
const pathname = url.pathname
|
|
76
|
+
|
|
77
|
+
// 1. Split the URL early to access its segments
|
|
78
|
+
const segments = pathname.split("/").filter(Boolean) // "/es/about/" -> ["es", "about"]
|
|
79
|
+
const firstSegment = segments[0] ?? null
|
|
80
|
+
|
|
81
|
+
// 2. SAFETY NET (For static file 404 pages)
|
|
82
|
+
// Attempt to infer the locale from the URL, or fallback to the default.
|
|
83
|
+
let safeLocale = DEFAULT_LOCALE
|
|
84
|
+
if (firstSegment) {
|
|
85
|
+
const parsed = LocaleSchema.safeParse(firstSegment)
|
|
86
|
+
if (parsed.success) {
|
|
87
|
+
safeLocale = parsed.data
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Inject the locale into the context BEFORE the bypass check!
|
|
92
|
+
context.locals.uiLocale = safeLocale
|
|
93
|
+
context.locals.translationLocale = resolveLocaleForTranslations(safeLocale)
|
|
94
|
+
|
|
95
|
+
// 3. Bypass static files and system paths (with Soft 404 protection)
|
|
96
|
+
if (shouldBypassI18n(pathname)) {
|
|
97
|
+
const response = await next()
|
|
98
|
+
|
|
99
|
+
const contentType = response.headers.get("content-type") || ""
|
|
100
|
+
const isHtml = contentType.includes("text/html")
|
|
101
|
+
const isRedirect = response.status >= 300 && response.status < 400
|
|
102
|
+
|
|
103
|
+
// If Astro attempts to return HTML for a static/API request (and it's not a redirect),
|
|
104
|
+
// it means the file wasn't found and Astro rendered a fallback page (404.astro or catch-all).
|
|
105
|
+
// We strictly intercept this and return a lightweight text-based 404 instead.
|
|
106
|
+
// Why? To mitigate Directory Bruteforcing / Fuzzing attacks and save resources.
|
|
107
|
+
if (isHtml && !isRedirect) {
|
|
108
|
+
// Aggressively save Worker CPU cycles for missing static files
|
|
109
|
+
return new Response("Not found", {
|
|
110
|
+
status: 404,
|
|
111
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return response
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fallbackLocale = resolveFallbackLocale(context)
|
|
119
|
+
|
|
120
|
+
// 1. No locale in URL — redirect to fallback locale root
|
|
121
|
+
if (!firstSegment) {
|
|
122
|
+
const target = buildLocalizedPath(fallbackLocale, [])
|
|
123
|
+
if (pathname !== target) {
|
|
124
|
+
// Apply headers to the redirect response
|
|
125
|
+
return applySecurityHeaders(context.redirect(target, 302))
|
|
126
|
+
}
|
|
127
|
+
return applySecurityHeaders(await next())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 2. URL has first segment — validate as locale
|
|
131
|
+
const parsed = LocaleSchema.safeParse(firstSegment)
|
|
132
|
+
const urlLocale: Locale | null = parsed.success ? parsed.data : null
|
|
133
|
+
|
|
134
|
+
if (urlLocale) {
|
|
135
|
+
// Keep cookie in sync with URL locale
|
|
136
|
+
setCookieLang(context.cookies, urlLocale)
|
|
137
|
+
// Set uiLocale to Astro locals
|
|
138
|
+
context.locals.uiLocale = urlLocale
|
|
139
|
+
// Define and set translationLocale to Astro locals
|
|
140
|
+
const translationLocale = resolveLocaleForTranslations(urlLocale)
|
|
141
|
+
context.locals.translationLocale = translationLocale
|
|
142
|
+
|
|
143
|
+
// Normalize trailing slash and structure
|
|
144
|
+
const normalized = buildLocalizedPath(urlLocale, segments.slice(1))
|
|
145
|
+
if (pathname !== normalized) {
|
|
146
|
+
return applySecurityHeaders(context.redirect(normalized, 302))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return applySecurityHeaders(await next())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 3. Invalid locale in URL -> treat the whole path as content missing the locale prefix.
|
|
153
|
+
const target = buildLocalizedPath(fallbackLocale, segments)
|
|
154
|
+
|
|
155
|
+
if (pathname !== target) {
|
|
156
|
+
return applySecurityHeaders(context.redirect(target, 302))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return applySecurityHeaders(await next())
|
|
160
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AstroGlobal } from "astro"
|
|
2
|
+
|
|
3
|
+
import { LocaleSchema, type Locale } from "./schema"
|
|
4
|
+
import { DEFAULT_LOCALE, ENABLE_MISSING_TRANSLATION_BANNER } from "./constants"
|
|
5
|
+
import { LOCALES_WITH_TRANSLATIONS, FULLY_TRANSLATED_LOCALES } from "./runtime-constants"
|
|
6
|
+
|
|
7
|
+
// Checking for the presence of at least one file with translations
|
|
8
|
+
function hasTranslations(locale: Locale): boolean {
|
|
9
|
+
return (LOCALES_WITH_TRANSLATIONS as readonly string[]).includes(locale)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Checking the completeness of translations
|
|
13
|
+
function isFullyTranslated(locale: Locale): boolean {
|
|
14
|
+
return (FULLY_TRANSLATED_LOCALES as readonly string[]).includes(locale)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Optional: return a “missing translation” banner message when uiLocale has no translations or
|
|
19
|
+
* (blog) content is missing in required language.
|
|
20
|
+
*
|
|
21
|
+
* Returns: - MissingTranslationType: type of message to show ('ui' or 'content') - null: if banner
|
|
22
|
+
* should not be shown
|
|
23
|
+
*/
|
|
24
|
+
type MissingTranslationType = "ui" | "content" | null
|
|
25
|
+
|
|
26
|
+
export function checkMissingTranslation(
|
|
27
|
+
uiLocale: Locale,
|
|
28
|
+
isMissingContent: boolean | undefined
|
|
29
|
+
): MissingTranslationType {
|
|
30
|
+
if (!ENABLE_MISSING_TRANSLATION_BANNER) return null
|
|
31
|
+
|
|
32
|
+
if (isFullyTranslated(uiLocale) && !isMissingContent) return null
|
|
33
|
+
|
|
34
|
+
return isMissingContent ? "content" : "ui"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* UI / routing locale:
|
|
39
|
+
*
|
|
40
|
+
* - What user selected
|
|
41
|
+
* - What is used for <html lang>, URL, etc.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveLocale(Astro: AstroGlobal): Locale {
|
|
44
|
+
const raw = Astro.currentLocale ?? DEFAULT_LOCALE
|
|
45
|
+
const parsed = LocaleSchema.safeParse(raw)
|
|
46
|
+
return parsed.success ? parsed.data : DEFAULT_LOCALE
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Translation locale: - What we actually load from KV - Falls back if there is no bundle for the UI
|
|
51
|
+
* locale
|
|
52
|
+
*/
|
|
53
|
+
export function resolveLocaleForTranslations(locale: Locale): Locale {
|
|
54
|
+
return hasTranslations(locale) ? locale : DEFAULT_LOCALE
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Auto-generated by scripts/bundle-translations.ts
|
|
2
|
+
// Do not edit manually.
|
|
3
|
+
|
|
4
|
+
export const NAMESPACE_KEYS = ['blog', 'chat', 'common', 'cookie_notice', 'editor', 'errors', 'home', 'messages', 'nav', 'projects', 'services'] as [string, ...string[]];
|
|
5
|
+
|
|
6
|
+
export const LOCALES_WITH_TRANSLATIONS = ['de', 'en', 'es', 'ja', 'pt'] as const;
|
|
7
|
+
|
|
8
|
+
export const FULLY_TRANSLATED_LOCALES = ['en', 'pt'] as const;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Monotonic translation bundle version.
|
|
12
|
+
* Updated on every `npm run i18n:bundle` run.
|
|
13
|
+
*/
|
|
14
|
+
export const TRANSLATIONS_VERSION = '6d16b1037964'
|
|
@@ -0,0 +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
|
|
@@ -0,0 +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
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +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
|
+
}
|