astro-awesomeness 1.1.1 → 1.2.0

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.
@@ -4,7 +4,7 @@ import { VariantProps } from "class-variance-authority";
4
4
  //#region src/components/ui/button-variants.d.ts
5
5
  declare const buttonVariants: (props?: ({
6
6
  size?: "default" | "icon" | "icon-lg" | "icon-sm" | "icon-xs" | "lg" | "sm" | "xs" | null | undefined;
7
- variant?: "default" | "link" | "destructive" | "ghost" | "outline" | "secondary" | null | undefined;
7
+ variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
8
8
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
9
9
  //#endregion
10
10
  //#region src/components/ui/button.d.ts
@@ -14,7 +14,7 @@ const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap
14
14
  variants: {
15
15
  size: {
16
16
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
17
- icon: "size-9",
17
+ icon: "size-11 sm:size-9",
18
18
  "icon-lg": "size-10",
19
19
  "icon-sm": "size-8",
20
20
  "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["ButtonPrimitive"],"sources":["../../src/components/ui/button-variants.ts","../../src/components/ui/button.tsx","../../src/components/theme-toggle.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n icon: \"size-9\",\n \"icon-lg\": \"size-10\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n },\n },\n },\n);\n\nexport { buttonVariants };\n","import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { buttonVariants } from \"./button-variants\";\n\ntype ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, \"className\"> &\n VariantProps<typeof buttonVariants> & {\n className?: string;\n };\n\nconst Button = ({ className, size = \"default\", variant = \"default\", ...props }: ButtonProps) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-size={size}\n data-slot=\"button\"\n data-variant={variant}\n {...props}\n />\n );\n};\n\nexport { Button };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst storedThemeListeners = new Set<() => void>();\n\nconst subscribeStoredTheme = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n storedThemeListeners.add(callback);\n window.addEventListener(\"storage\", callback);\n return () => {\n storedThemeListeners.delete(callback);\n window.removeEventListener(\"storage\", callback);\n };\n};\n\nconst setStoredTheme = (next: Theme) => {\n window.localStorage.setItem(\"theme\", next);\n for (const listener of storedThemeListeners) {\n listener();\n }\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setStoredTheme(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleToggleTheme} size=\"icon\" variant=\"ghost\">\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":";;;;;;;;AAEA,MAAM,iBAAiB,IACrB,+bACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SAAS;GACT,MAAM;GACN,WAAW;GACX,WAAW;GACX,WAAW;GACX,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,SAAS;GACP,SAAS;GACT,aACE;GACF,OAAO;GACP,MAAM;GACN,SACE;GACF,WAAW;EACb;CACF;AACF,CACF;;;;ACpBA,MAAM,UAAU,EAAE,WAAW,OAAO,WAAW,UAAU,WAAW,GAAG,YAAyB;CAC9F,OACE,oBAACA,UAAD;EACE,WAAW,GAAG,eAAe;GAAE;GAAW;GAAM;EAAQ,CAAC,CAAC;EAC1D,aAAW;EACX,aAAU;EACV,gBAAc;EACd,GAAI;CACL;AAEL;;;;ACfA,MAAM,uBAAqC;CACzC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,MAAM,SAAS,OAAO,aAAa,QAAQ,OAAO;CAClD,IAAI,WAAW,WAAW,WAAW,QACnC,OAAO;CAET,OAAO;AACT;AAEA,MAAM,uCAAuB,IAAI,IAAgB;AAEjD,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,qBAAqB,IAAI,QAAQ;CACjC,OAAO,iBAAiB,WAAW,QAAQ;CAC3C,aAAa;EACX,qBAAqB,OAAO,QAAQ;EACpC,OAAO,oBAAoB,WAAW,QAAQ;CAChD;AACF;AAEA,MAAM,kBAAkB,SAAgB;CACtC,OAAO,aAAa,QAAQ,SAAS,IAAI;CACzC,KAAK,MAAM,YAAY,sBACrB,SAAS;AAEb;AAEA,MAAM,uBAAgC;CACpC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,OAAO,OAAO,WAAW,8BAA8B,CAAC,CAAC;AAC3D;AAEA,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,MAAM,QAAQ,OAAO,WAAW,8BAA8B;CAC9D,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa;EACX,MAAM,oBAAoB,UAAU,QAAQ;CAC9C;AACF;AAMA,MAAM,eAAe,EAAE,YAAY,qBAA4B;CAC7D,MAAM,cAAc,qBAAqB,sBAAsB,sBAAsB,KAAK;CAE1F,MAAM,QADW,qBAAqB,sBAAsB,sBAAsB,IACtD,MAAM,cAAc,SAAS;CAEzD,MAAM,0BAA0B;EAC9B,MAAM,OAAc,UAAU,SAAS,UAAU;EACjD,SAAS,gBAAgB,UAAU,OAAO,QAAQ,SAAS,MAAM;EACjE,eAAe,IAAI;CACrB;CAEA,OACE,oBAAC,QAAD;EAAQ,cAAY;EAAW,SAAS;EAAmB,MAAK;EAAO,SAAQ;YAC5E,UAAU,SAAS,oBAAC,KAAD,CAAM,KAAI,oBAAC,MAAD,CAAO;CAC/B;AAEZ"}
1
+ {"version":3,"file":"index.js","names":["ButtonPrimitive"],"sources":["../../src/components/ui/button-variants.ts","../../src/components/ui/button.tsx","../../src/components/theme-toggle.tsx"],"sourcesContent":["import { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n {\n defaultVariants: {\n size: \"default\",\n variant: \"default\",\n },\n variants: {\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n icon: \"size-11 sm:size-9\",\n \"icon-lg\": \"size-10\",\n \"icon-sm\": \"size-8\",\n \"icon-xs\": \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n },\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n ghost: \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n secondary: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n },\n },\n },\n);\n\nexport { buttonVariants };\n","import { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"../../lib/cn\";\nimport { buttonVariants } from \"./button-variants\";\n\ntype ButtonProps = Omit<React.ComponentProps<typeof ButtonPrimitive>, \"className\"> &\n VariantProps<typeof buttonVariants> & {\n className?: string;\n };\n\nconst Button = ({ className, size = \"default\", variant = \"default\", ...props }: ButtonProps) => {\n return (\n <ButtonPrimitive\n className={cn(buttonVariants({ className, size, variant }))}\n data-size={size}\n data-slot=\"button\"\n data-variant={variant}\n {...props}\n />\n );\n};\n\nexport { Button };\n","import { Moon, Sun } from \"lucide-react\";\nimport { useSyncExternalStore } from \"react\";\n\nimport { Button } from \"./ui/button\";\n\ntype Theme = \"light\" | \"dark\";\n\nconst getStoredTheme = (): Theme | null => {\n if (typeof window === \"undefined\") {\n return null;\n }\n const stored = window.localStorage.getItem(\"theme\");\n if (stored === \"light\" || stored === \"dark\") {\n return stored;\n }\n return null;\n};\n\nconst storedThemeListeners = new Set<() => void>();\n\nconst subscribeStoredTheme = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n storedThemeListeners.add(callback);\n window.addEventListener(\"storage\", callback);\n return () => {\n storedThemeListeners.delete(callback);\n window.removeEventListener(\"storage\", callback);\n };\n};\n\nconst setStoredTheme = (next: Theme) => {\n window.localStorage.setItem(\"theme\", next);\n for (const listener of storedThemeListeners) {\n listener();\n }\n};\n\nconst getPrefersDark = (): boolean => {\n if (typeof window === \"undefined\") {\n return false;\n }\n return window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n};\n\nconst subscribePrefersDark = (callback: () => void) => {\n if (typeof window === \"undefined\") {\n return () => {};\n }\n const media = window.matchMedia(\"(prefers-color-scheme: dark)\");\n media.addEventListener(\"change\", callback);\n return () => {\n media.removeEventListener(\"change\", callback);\n };\n};\n\ntype Props = {\n ariaLabel?: string;\n};\n\nconst ThemeToggle = ({ ariaLabel = \"Toggle theme\" }: Props) => {\n const prefersDark = useSyncExternalStore(subscribePrefersDark, getPrefersDark, () => false);\n const override = useSyncExternalStore(subscribeStoredTheme, getStoredTheme, () => null);\n const theme: Theme = override ?? (prefersDark ? \"dark\" : \"light\");\n\n const handleToggleTheme = () => {\n const next: Theme = theme === \"dark\" ? \"light\" : \"dark\";\n document.documentElement.classList.toggle(\"dark\", next === \"dark\");\n setStoredTheme(next);\n };\n\n return (\n <Button aria-label={ariaLabel} onClick={handleToggleTheme} size=\"icon\" variant=\"ghost\">\n {theme === \"dark\" ? <Sun /> : <Moon />}\n </Button>\n );\n};\n\nexport { ThemeToggle };\n"],"mappings":";;;;;;;;AAEA,MAAM,iBAAiB,IACrB,+bACA;CACE,iBAAiB;EACf,MAAM;EACN,SAAS;CACX;CACA,UAAU;EACR,MAAM;GACJ,SAAS;GACT,MAAM;GACN,WAAW;GACX,WAAW;GACX,WAAW;GACX,IAAI;GACJ,IAAI;GACJ,IAAI;EACN;EACA,SAAS;GACP,SAAS;GACT,aACE;GACF,OAAO;GACP,MAAM;GACN,SACE;GACF,WAAW;EACb;CACF;AACF,CACF;;;;ACpBA,MAAM,UAAU,EAAE,WAAW,OAAO,WAAW,UAAU,WAAW,GAAG,YAAyB;CAC9F,OACE,oBAACA,UAAD;EACE,WAAW,GAAG,eAAe;GAAE;GAAW;GAAM;EAAQ,CAAC,CAAC;EAC1D,aAAW;EACX,aAAU;EACV,gBAAc;EACd,GAAI;CACL;AAEL;;;;ACfA,MAAM,uBAAqC;CACzC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,MAAM,SAAS,OAAO,aAAa,QAAQ,OAAO;CAClD,IAAI,WAAW,WAAW,WAAW,QACnC,OAAO;CAET,OAAO;AACT;AAEA,MAAM,uCAAuB,IAAI,IAAgB;AAEjD,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,qBAAqB,IAAI,QAAQ;CACjC,OAAO,iBAAiB,WAAW,QAAQ;CAC3C,aAAa;EACX,qBAAqB,OAAO,QAAQ;EACpC,OAAO,oBAAoB,WAAW,QAAQ;CAChD;AACF;AAEA,MAAM,kBAAkB,SAAgB;CACtC,OAAO,aAAa,QAAQ,SAAS,IAAI;CACzC,KAAK,MAAM,YAAY,sBACrB,SAAS;AAEb;AAEA,MAAM,uBAAgC;CACpC,IAAI,OAAO,WAAW,aACpB,OAAO;CAET,OAAO,OAAO,WAAW,8BAA8B,CAAC,CAAC;AAC3D;AAEA,MAAM,wBAAwB,aAAyB;CACrD,IAAI,OAAO,WAAW,aACpB,aAAa,CAAC;CAEhB,MAAM,QAAQ,OAAO,WAAW,8BAA8B;CAC9D,MAAM,iBAAiB,UAAU,QAAQ;CACzC,aAAa;EACX,MAAM,oBAAoB,UAAU,QAAQ;CAC9C;AACF;AAMA,MAAM,eAAe,EAAE,YAAY,qBAA4B;CAC7D,MAAM,cAAc,qBAAqB,sBAAsB,sBAAsB,KAAK;CAE1F,MAAM,QADW,qBAAqB,sBAAsB,sBAAsB,IACtD,MAAM,cAAc,SAAS;CAEzD,MAAM,0BAA0B;EAC9B,MAAM,OAAc,UAAU,SAAS,UAAU;EACjD,SAAS,gBAAgB,UAAU,OAAO,QAAQ,SAAS,MAAM;EACjE,eAAe,IAAI;CACrB;CAEA,OACE,oBAAC,QAAD;EAAQ,cAAY;EAAW,SAAS;EAAmB,MAAK;EAAO,SAAQ;YAC5E,UAAU,SAAS,oBAAC,KAAD,CAAM,KAAI,oBAAC,MAAD,CAAO;CAC/B;AAEZ"}
@@ -2,8 +2,19 @@ import { z } from "zod";
2
2
  declare namespace index_d_exports {
3
3
  export { Author, Post, Tag, authorSchema, byPubDateDesc, notDraft, postSchema, tagSchema };
4
4
  }
5
+ declare const authorSchema: z.ZodObject<{
6
+ bio: z.ZodOptional<z.ZodString>;
7
+ name: z.ZodString;
8
+ photoUrl: z.ZodOptional<z.ZodString>;
9
+ url: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>;
5
11
  declare const postSchema: z.ZodObject<{
6
- author: z.ZodOptional<z.ZodString>;
12
+ author: z.ZodOptional<z.ZodObject<{
13
+ bio: z.ZodOptional<z.ZodString>;
14
+ name: z.ZodString;
15
+ photoUrl: z.ZodOptional<z.ZodString>;
16
+ url: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>>;
7
18
  categories: z.ZodDefault<z.ZodArray<z.ZodString>>;
8
19
  cover: z.ZodOptional<z.ZodString>;
9
20
  coverAlt: z.ZodOptional<z.ZodString>;
@@ -27,12 +38,6 @@ declare const tagSchema: z.ZodObject<{
27
38
  description: z.ZodOptional<z.ZodString>;
28
39
  name: z.ZodString;
29
40
  }, z.core.$strip>;
30
- declare const authorSchema: z.ZodObject<{
31
- avatar: z.ZodOptional<z.ZodString>;
32
- bio: z.ZodOptional<z.ZodString>;
33
- name: z.ZodString;
34
- url: z.ZodOptional<z.ZodURL>;
35
- }, z.core.$strip>;
36
41
  type Post = z.infer<typeof postSchema>;
37
42
  type Tag = z.infer<typeof tagSchema>;
38
43
  type Author = z.infer<typeof authorSchema>;
@@ -9,8 +9,14 @@ var content_exports = /* @__PURE__ */ __exportAll({
9
9
  postSchema: () => postSchema,
10
10
  tagSchema: () => tagSchema
11
11
  });
12
+ const authorSchema = z.object({
13
+ bio: z.string().optional(),
14
+ name: z.string().min(1),
15
+ photoUrl: z.string().optional(),
16
+ url: z.string().optional()
17
+ });
12
18
  const postSchema = z.object({
13
- author: z.string().optional(),
19
+ author: authorSchema.optional(),
14
20
  categories: z.array(z.string()).default([]),
15
21
  cover: z.string().optional(),
16
22
  coverAlt: z.string().optional(),
@@ -32,12 +38,6 @@ const tagSchema = z.object({
32
38
  description: z.string().optional(),
33
39
  name: z.string().min(1)
34
40
  });
35
- const authorSchema = z.object({
36
- avatar: z.string().optional(),
37
- bio: z.string().optional(),
38
- name: z.string().min(1),
39
- url: z.url().optional()
40
- });
41
41
  const notDraft = (post) => post.data.status !== "DRAFT" && post.data.draft !== true;
42
42
  const byPubDateDesc = (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf();
43
43
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/content/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\nconst postSchema = z.object({\n author: z.string().optional(),\n categories: z.array(z.string()).default([]),\n cover: z.string().optional(),\n coverAlt: z.string().optional(),\n description: z.string().default(\"\"),\n draft: z.boolean().default(false),\n heroImageHeight: z.number().int().optional(),\n heroImageUrl: z.url().optional(),\n heroImageWidth: z.number().int().optional(),\n pubDate: z.coerce.date(),\n seo: z\n .object({\n canonical_url: z.string().optional(),\n })\n .optional(),\n series: z.string().optional(),\n slug: z.string().optional(),\n status: z.string().optional(),\n tags: z.array(z.string()).default([]),\n title: z.string().min(1),\n updatedDate: z.coerce.date().optional(),\n});\n\nconst tagSchema = z.object({\n description: z.string().optional(),\n name: z.string().min(1),\n});\n\nconst authorSchema = z.object({\n avatar: z.string().optional(),\n bio: z.string().optional(),\n name: z.string().min(1),\n url: z.url().optional(),\n});\n\ntype Post = z.infer<typeof postSchema>;\ntype Tag = z.infer<typeof tagSchema>;\ntype Author = z.infer<typeof authorSchema>;\n\nconst notDraft = (post: { data: { draft?: boolean; status?: string } }): boolean =>\n post.data.status !== \"DRAFT\" && post.data.draft !== true;\n\nconst byPubDateDesc = (a: { data: { pubDate: Date } }, b: { data: { pubDate: Date } }): number =>\n b.data.pubDate.valueOf() - a.data.pubDate.valueOf();\n\nexport { authorSchema, byPubDateDesc, notDraft, postSchema, tagSchema };\nexport type { Author, Post, Tag };\n"],"mappings":";;;;;;;;;;;AAEA,MAAM,aAAa,EAAE,OAAO;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1C,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,aAAa,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE;CAClC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAChC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC3C,cAAc,EAAE,IAAI,CAAC,CAAC,SAAS;CAC/B,gBAAgB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC1C,SAAS,EAAE,OAAO,KAAK;CACvB,KAAK,EACF,OAAO,EACN,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS,EACrC,CAAC,CAAC,CACD,SAAS;CACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,aAAa,EAAE,OAAO,KAAK,CAAC,CAAC,SAAS;AACxC,CAAC;AAED,MAAM,YAAY,EAAE,OAAO;CACzB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AACxB,CAAC;AAED,MAAM,eAAe,EAAE,OAAO;CAC5B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,KAAK,EAAE,IAAI,CAAC,CAAC,SAAS;AACxB,CAAC;AAMD,MAAM,YAAY,SAChB,KAAK,KAAK,WAAW,WAAW,KAAK,KAAK,UAAU;AAEtD,MAAM,iBAAiB,GAAgC,MACrD,EAAE,KAAK,QAAQ,QAAQ,IAAI,EAAE,KAAK,QAAQ,QAAQ"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/content/index.ts"],"sourcesContent":["import { z } from \"zod\";\n\n// Mirrors @easeia/astro-content's BuildAuthor after its loader strips nulls\n// to undefined. Plain strings (not z.url()) on purpose: a typo in the admin's\n// author URL must not fail 12 site builds.\nconst authorSchema = z.object({\n bio: z.string().optional(),\n name: z.string().min(1),\n photoUrl: z.string().optional(),\n url: z.string().optional(),\n});\n\nconst postSchema = z.object({\n author: authorSchema.optional(),\n categories: z.array(z.string()).default([]),\n cover: z.string().optional(),\n coverAlt: z.string().optional(),\n description: z.string().default(\"\"),\n draft: z.boolean().default(false),\n heroImageHeight: z.number().int().optional(),\n heroImageUrl: z.url().optional(),\n heroImageWidth: z.number().int().optional(),\n pubDate: z.coerce.date(),\n seo: z\n .object({\n canonical_url: z.string().optional(),\n })\n .optional(),\n series: z.string().optional(),\n slug: z.string().optional(),\n status: z.string().optional(),\n tags: z.array(z.string()).default([]),\n title: z.string().min(1),\n updatedDate: z.coerce.date().optional(),\n});\n\nconst tagSchema = z.object({\n description: z.string().optional(),\n name: z.string().min(1),\n});\n\ntype Post = z.infer<typeof postSchema>;\ntype Tag = z.infer<typeof tagSchema>;\ntype Author = z.infer<typeof authorSchema>;\n\nconst notDraft = (post: { data: { draft?: boolean; status?: string } }): boolean =>\n post.data.status !== \"DRAFT\" && post.data.draft !== true;\n\nconst byPubDateDesc = (a: { data: { pubDate: Date } }, b: { data: { pubDate: Date } }): number =>\n b.data.pubDate.valueOf() - a.data.pubDate.valueOf();\n\nexport { authorSchema, byPubDateDesc, notDraft, postSchema, tagSchema };\nexport type { Author, Post, Tag };\n"],"mappings":";;;;;;;;;;;AAKA,MAAM,eAAe,EAAE,OAAO;CAC5B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACtB,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,KAAK,EAAE,OAAO,CAAC,CAAC,SAAS;AAC3B,CAAC;AAED,MAAM,aAAa,EAAE,OAAO;CAC1B,QAAQ,aAAa,SAAS;CAC9B,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CAC1C,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,aAAa,EAAE,OAAO,CAAC,CAAC,QAAQ,EAAE;CAClC,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,KAAK;CAChC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC3C,cAAc,EAAE,IAAI,CAAC,CAAC,SAAS;CAC/B,gBAAgB,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS;CAC1C,SAAS,EAAE,OAAO,KAAK;CACvB,KAAK,EACF,OAAO,EACN,eAAe,EAAE,OAAO,CAAC,CAAC,SAAS,EACrC,CAAC,CAAC,CACD,SAAS;CACZ,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS;CAC1B,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;CACpC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACvB,aAAa,EAAE,OAAO,KAAK,CAAC,CAAC,SAAS;AACxC,CAAC;AAED,MAAM,YAAY,EAAE,OAAO;CACzB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;AACxB,CAAC;AAMD,MAAM,YAAY,SAChB,KAAK,KAAK,WAAW,WAAW,KAAK,KAAK,UAAU;AAEtD,MAAM,iBAAiB,GAAgC,MACrD,EAAE,KAAK,QAAQ,QAAQ,IAAI,EAAE,KAAK,QAAQ,QAAQ"}
@@ -1,4 +1,7 @@
1
1
  import { cn } from "cnfast";
2
+ //#region src/lib/category-label.d.ts
3
+ declare const categoryLabel: (name: string) => string;
4
+ //#endregion
2
5
  //#region src/lib/format-date.d.ts
3
6
  declare const formatDate: (date: Date, locale?: string, options?: Intl.DateTimeFormatOptions) => string;
4
7
  //#endregion
@@ -30,7 +33,6 @@ type PostUrlBuilder = {
30
33
  postUrl: (post: PostLike) => string;
31
34
  };
32
35
  declare const createPostUrl: (categorySlugMap: Record<string, string>, defaultCategory: string) => PostUrlBuilder;
33
- declare const categoryLabel: (name: string) => string;
34
36
  //#endregion
35
37
  //#region src/lib/reading-time.d.ts
36
38
  declare const readingTime: (input: string) => {
@@ -51,5 +53,5 @@ declare namespace index_d_exports {
51
53
  export { DEFAULT_TAG_BASE, PostLike, PostParams, PostUrlBuilder, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
52
54
  }
53
55
  //#endregion
54
- export { requireEnv as a, PostParams as c, createPostUrl as d, getRelatedPosts as f, slugify as i, PostUrlBuilder as l, cn as m, DEFAULT_TAG_BASE as n, readingTime as o, formatDate as p, tagUrl as r, PostLike as s, index_d_exports as t, categoryLabel as u };
55
- //# sourceMappingURL=index-BHnvZ16j.d.ts.map
56
+ export { requireEnv as a, PostParams as c, getRelatedPosts as d, formatDate as f, slugify as i, PostUrlBuilder as l, cn as m, DEFAULT_TAG_BASE as n, readingTime as o, categoryLabel as p, tagUrl as r, PostLike as s, index_d_exports as t, createPostUrl as u };
57
+ //# sourceMappingURL=index-CrcndL2W.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { Author, Post, Tag, t as index_d_exports } from "./content/index.js";
2
- import { t as index_d_exports$1 } from "./index-BHnvZ16j.js";
2
+ import { t as index_d_exports$1 } from "./index-CrcndL2W.js";
3
3
  export { type Author, type Post, type Tag, index_d_exports as content, index_d_exports$1 as lib };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { t as content_exports } from "./content/index.js";
2
- import { t as lib_exports } from "./lib-CBjp3x7a.js";
2
+ import { t as lib_exports } from "./lib-BqEjsOJx.js";
3
3
 
4
4
  export { content_exports as content, lib_exports as lib };
@@ -1,2 +1,2 @@
1
- import { a as requireEnv, c as PostParams, d as createPostUrl, f as getRelatedPosts, i as slugify, l as PostUrlBuilder, m as cn, n as DEFAULT_TAG_BASE, o as readingTime, p as formatDate, r as tagUrl, s as PostLike, u as categoryLabel } from "../index-BHnvZ16j.js";
1
+ import { a as requireEnv, c as PostParams, d as getRelatedPosts, f as formatDate, i as slugify, l as PostUrlBuilder, m as cn, n as DEFAULT_TAG_BASE, o as readingTime, p as categoryLabel, r as tagUrl, s as PostLike, u as createPostUrl } from "../index-CrcndL2W.js";
2
2
  export { DEFAULT_TAG_BASE, type PostLike, type PostParams, type PostUrlBuilder, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
package/dist/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { t as cn } from "../cn-DmzdyV07.js";
2
- import { a as readingTime, c as slugify, i as requireEnv, l as getRelatedPosts, n as DEFAULT_TAG_BASE, o as categoryLabel, r as tagUrl, s as createPostUrl, u as formatDate } from "../lib-CBjp3x7a.js";
2
+ import { a as readingTime, c as getRelatedPosts, i as requireEnv, l as formatDate, n as DEFAULT_TAG_BASE, o as createPostUrl, r as tagUrl, s as slugify, u as categoryLabel } from "../lib-BqEjsOJx.js";
3
3
 
4
4
  export { DEFAULT_TAG_BASE, categoryLabel, cn, createPostUrl, formatDate, getRelatedPosts, readingTime, requireEnv, slugify, tagUrl };
@@ -1,6 +1,38 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-w6R9maHv.js";
2
2
  import { t as cn } from "./cn-DmzdyV07.js";
3
3
 
4
+ //#region src/lib/category-label.ts
5
+ const NAMED_ENTITIES = {
6
+ amp: "&",
7
+ apos: "'",
8
+ gt: ">",
9
+ hellip: "…",
10
+ ldquo: "“",
11
+ lsquo: "‘",
12
+ lt: "<",
13
+ nbsp: "\xA0",
14
+ ndash: "–",
15
+ quot: "\"",
16
+ rdquo: "”",
17
+ rsquo: "’"
18
+ };
19
+ const MAX_CODE_POINT = 1114111;
20
+ const EM_DASH = 8212;
21
+ const HEX_RADIX = 16;
22
+ const ENTITY_PATTERN = new RegExp("&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));", "giv");
23
+ const isNamedEntity = (key) => Object.hasOwn(NAMED_ENTITIES, key);
24
+ const fromCodePoint = (codePoint, entity) => Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH ? String.fromCodePoint(codePoint) : entity;
25
+ const decodeEntity = (entity, decimal, hex, named) => {
26
+ if (decimal !== void 0) return fromCodePoint(Number(decimal), entity);
27
+ if (hex !== void 0) return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
28
+ if (named === void 0) return entity;
29
+ const key = named.toLowerCase();
30
+ if (!isNamedEntity(key)) return entity;
31
+ return NAMED_ENTITIES[key];
32
+ };
33
+ const categoryLabel = (name) => name.replaceAll(ENTITY_PATTERN, decodeEntity);
34
+
35
+ //#endregion
4
36
  //#region src/lib/format-date.ts
5
37
  const defaultOptions = {
6
38
  day: "numeric",
@@ -76,33 +108,6 @@ const createPostUrl = (categorySlugMap, defaultCategory) => {
76
108
  postUrl
77
109
  };
78
110
  };
79
- const NAMED_ENTITIES = {
80
- amp: "&",
81
- apos: "'",
82
- gt: ">",
83
- hellip: "…",
84
- ldquo: "“",
85
- lsquo: "‘",
86
- lt: "<",
87
- nbsp: "\xA0",
88
- ndash: "–",
89
- quot: "\"",
90
- rdquo: "”",
91
- rsquo: "’"
92
- };
93
- const MAX_CODE_POINT = 1114111;
94
- const EM_DASH = 8212;
95
- const HEX_RADIX = 16;
96
- const ENTITY_PATTERN = new RegExp("&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));", "giv");
97
- const fromCodePoint = (codePoint, entity) => Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH ? String.fromCodePoint(codePoint) : entity;
98
- const decodeEntity = (entity, decimal, hex, named) => {
99
- if (decimal !== void 0) return fromCodePoint(Number(decimal), entity);
100
- if (hex !== void 0) return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
101
- if (named === void 0) return entity;
102
- const key = named.toLowerCase();
103
- return Object.hasOwn(NAMED_ENTITIES, key) ? NAMED_ENTITIES[key] ?? entity : entity;
104
- };
105
- const categoryLabel = (name) => name.replaceAll(ENTITY_PATTERN, decodeEntity);
106
111
 
107
112
  //#endregion
108
113
  //#region src/lib/reading-time.ts
@@ -145,5 +150,5 @@ var lib_exports = /* @__PURE__ */ __exportAll({
145
150
  });
146
151
 
147
152
  //#endregion
148
- export { readingTime as a, slugify as c, requireEnv as i, getRelatedPosts as l, DEFAULT_TAG_BASE as n, categoryLabel as o, tagUrl as r, createPostUrl as s, lib_exports as t, formatDate as u };
149
- //# sourceMappingURL=lib-CBjp3x7a.js.map
153
+ export { readingTime as a, getRelatedPosts as c, requireEnv as i, formatDate as l, DEFAULT_TAG_BASE as n, createPostUrl as o, tagUrl as r, slugify as s, lib_exports as t, categoryLabel as u };
154
+ //# sourceMappingURL=lib-BqEjsOJx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-BqEjsOJx.js","names":[],"sources":["../src/lib/category-label.ts","../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/require-env.ts","../src/lib/tag-url.ts","../src/lib/index.ts"],"sourcesContent":["const NAMED_ENTITIES = {\n amp: \"&\",\n apos: \"'\",\n gt: \">\",\n hellip: \"\\u2026\",\n ldquo: \"\\u201c\",\n lsquo: \"\\u2018\",\n lt: \"<\",\n nbsp: \"\\u00a0\",\n ndash: \"\\u2013\",\n quot: '\"',\n rdquo: \"\\u201d\",\n rsquo: \"\\u2019\",\n} as const;\n\nconst MAX_CODE_POINT = 1_114_111;\nconst EM_DASH = 8212;\nconst HEX_RADIX = 16;\nconst ENTITY_PATTERN = /&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));/giv;\n\nconst isNamedEntity = (key: string): key is keyof typeof NAMED_ENTITIES =>\n Object.hasOwn(NAMED_ENTITIES, key);\n\nconst fromCodePoint = (codePoint: number, entity: string): string =>\n Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH\n ? String.fromCodePoint(codePoint)\n : entity;\n\nconst decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {\n if (decimal !== undefined) {\n return fromCodePoint(Number(decimal), entity);\n }\n if (hex !== undefined) {\n return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);\n }\n if (named === undefined) {\n return entity;\n }\n const key = named.toLowerCase();\n if (!isNamedEntity(key)) {\n return entity;\n }\n return NAMED_ENTITIES[key];\n};\n\nconst categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);\n\nexport { categoryLabel };\n","const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (key: string, locale: string, options: Intl.DateTimeFormatOptions) => {\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (date: Date, locale: string = \"en-US\", options?: Intl.DateTimeFormatOptions) => {\n if (!options) {\n return getFormatter(locale, locale, defaultOptions).format(date);\n }\n const merged = { ...defaultOptions, ...options };\n return getFormatter(`${locale}:${JSON.stringify(merged)}`, locale, merged).format(date);\n};\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; id: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.id === current.id) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical !== undefined && canonical !== \"\") {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category !== undefined && slug !== undefined) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url; fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category =\n first !== undefined && first !== \"\"\n ? (categorySlugMap[first] ?? slugify(first))\n : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","const requireEnv = (name: string): string => {\n const value = process.env[name];\n if (value === undefined || value === \"\") {\n throw new Error(`Missing required env var: ${name}`);\n }\n return value;\n};\n\nexport { requireEnv };\n","import { slugify } from \"./slugify\";\n\nconst DEFAULT_TAG_BASE = \"/tag\";\n\nconst tagUrl = (tag: string, base: string = DEFAULT_TAG_BASE): string =>\n `${base.replace(/\\/+$/v, \"\")}/${slugify(tag)}/`;\n\nexport { DEFAULT_TAG_BASE, tagUrl };\n","export { cn } from \"./cn\";\nexport { categoryLabel } from \"./category-label\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { requireEnv } from \"./require-env\";\nexport { slugify } from \"./slugify\";\nexport { DEFAULT_TAG_BASE, tagUrl } from \"./tag-url\";\n"],"mappings":"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"}
@@ -100,6 +100,7 @@
100
100
  }
101
101
  html {
102
102
  -webkit-text-size-adjust: 100%;
103
+ scrollbar-gutter: stable;
103
104
  }
104
105
  body {
105
106
  background-color: var(--color-background);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-awesomeness",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Astro 6/7 blog theme with shadcn primitives and a Tailwind v4 preset.",
5
5
  "keywords": [
6
6
  "astro",
@@ -66,10 +66,10 @@
66
66
  "@types/react-dom": "^19.2.3",
67
67
  "@vitest/coverage-v8": "^4.1.10",
68
68
  "tailwindcss": "^4.3.2",
69
- "typescript": "^6.0.3",
69
+ "typescript": "^7.0.2",
70
70
  "vitest": "^4.1.10",
71
- "@repo/config-vitest": "0.0.0",
72
- "@repo/typescript-config": "0.0.0"
71
+ "@repo/typescript-config": "0.0.0",
72
+ "@repo/config-vitest": "0.0.0"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "astro": "^6.0.0 || ^7.0.0",
@@ -0,0 +1,34 @@
1
+ ---
2
+ import type { Author } from "../content";
3
+
4
+ type Props = {
5
+ author: Author;
6
+ class?: string;
7
+ };
8
+
9
+ const { author, class: className = "" } = Astro.props;
10
+ ---
11
+ <div class:list={["flex items-center gap-2", className]}>
12
+ {author.photoUrl && (
13
+ <img
14
+ src={author.photoUrl}
15
+ alt={author.name}
16
+ width="28"
17
+ height="28"
18
+ loading="lazy"
19
+ decoding="async"
20
+ class="border-border size-7 rounded-full border object-cover"
21
+ />
22
+ )}
23
+ {author.url ? (
24
+ <a
25
+ href={author.url}
26
+ rel="author"
27
+ class="text-foreground text-sm font-medium hover:underline"
28
+ >
29
+ {author.name}
30
+ </a>
31
+ ) : (
32
+ <span class="text-foreground text-sm font-medium">{author.name}</span>
33
+ )}
34
+ </div>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import type { Author } from "../content";
3
+
4
+ type Props = {
5
+ author: Author;
6
+ class?: string;
7
+ };
8
+
9
+ const { author, class: className = "" } = Astro.props;
10
+ ---
11
+ <aside class:list={["border-border bg-muted/50 flex gap-4 rounded-lg border p-4", className]}>
12
+ {author.photoUrl && (
13
+ <img
14
+ src={author.photoUrl}
15
+ alt={author.name}
16
+ width="56"
17
+ height="56"
18
+ loading="lazy"
19
+ decoding="async"
20
+ class="border-border size-14 shrink-0 rounded-full border object-cover"
21
+ />
22
+ )}
23
+ <div class="flex flex-col gap-1">
24
+ {author.url ? (
25
+ <a href={author.url} rel="author" class="text-foreground font-semibold hover:underline">
26
+ {author.name}
27
+ </a>
28
+ ) : (
29
+ <span class="text-foreground font-semibold">{author.name}</span>
30
+ )}
31
+ {author.bio && <p class="text-muted-foreground text-sm">{author.bio}</p>}
32
+ </div>
33
+ </aside>
@@ -8,7 +8,7 @@ const { siteTitle, class: className = "" } = Astro.props;
8
8
  const year = new Date().getFullYear();
9
9
  ---
10
10
  <footer class:list={["border-border text-muted-foreground mt-16 border-t py-6 text-sm", className]}>
11
- <div class="mx-auto flex max-w-3xl flex-col items-center justify-between gap-2 px-4 sm:flex-row">
11
+ <div class="mx-auto flex max-w-5xl flex-col items-center justify-between gap-2 px-5 sm:flex-row sm:px-8">
12
12
  <p>© {year} {siteTitle}</p>
13
13
  <slot name="links" />
14
14
  </div>
@@ -18,8 +18,8 @@ const {
18
18
  } = Astro.props;
19
19
  ---
20
20
  <header class:list={["border-border bg-background/80 sticky top-0 z-40 border-b backdrop-blur", className]}>
21
- <div class="mx-auto flex max-w-3xl items-center justify-between px-4 py-3">
22
- <a href={homeHref} class="text-primary text-base font-semibold tracking-tight">
21
+ <div class="mx-auto flex max-w-5xl items-center justify-between px-5 py-4 sm:px-8">
22
+ <a href={homeHref} class="text-foreground font-serif text-lg font-semibold tracking-tight">
23
23
  <slot name="brand">{siteTitle}</slot>
24
24
  </a>
25
25
  <nav class="flex items-center gap-2">
@@ -9,6 +9,7 @@ type Props = {
9
9
  locale?: string;
10
10
  publishedTime?: Date;
11
11
  modifiedTime?: Date;
12
+ authorName?: string;
12
13
  };
13
14
 
14
15
  const {
@@ -21,11 +22,13 @@ const {
21
22
  locale = "en_US",
22
23
  publishedTime,
23
24
  modifiedTime,
25
+ authorName,
24
26
  } = Astro.props;
25
27
  const url = canonical ?? new URL(Astro.url.pathname, Astro.site ?? Astro.url.origin).toString();
26
28
  ---
27
29
  <title>{title}</title>
28
30
  <meta name="description" content={description} />
31
+ {authorName && <meta name="author" content={authorName} />}
29
32
  <link rel="canonical" href={url} />
30
33
 
31
34
  <meta property="og:type" content={type} />
@@ -10,7 +10,7 @@ const buttonVariants = cva(
10
10
  variants: {
11
11
  size: {
12
12
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
13
- icon: "size-9",
13
+ icon: "size-11 sm:size-9",
14
14
  "icon-lg": "size-10",
15
15
  "icon-sm": "size-8",
16
16
  "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
@@ -61,15 +61,15 @@ describe("postSchema", () => {
61
61
  }
62
62
  });
63
63
 
64
- it("accepts a top-level author", () => {
64
+ it("accepts a top-level author entity", () => {
65
65
  const result = postSchema.safeParse({
66
- author: "Pedro",
66
+ author: { bio: "Records rooms.", name: "Pedro", url: "https://example.com/pedro" },
67
67
  pubDate: new Date(),
68
68
  title: "Hello",
69
69
  });
70
70
  expect(result.success).toBe(true);
71
71
  if (result.success) {
72
- expect(result.data.author).toBe("Pedro");
72
+ expect(result.data.author?.name).toBe("Pedro");
73
73
  }
74
74
  });
75
75
  });
@@ -106,8 +106,10 @@ describe("authorSchema", () => {
106
106
  it("accepts a minimal author", () => {
107
107
  expect(authorSchema.safeParse({ name: "Pedro" }).success).toBe(true);
108
108
  });
109
- it("validates url field as URL string", () => {
110
- const result = authorSchema.safeParse({ name: "Pedro", url: "not-a-url" });
111
- expect(result.success).toBe(false);
109
+ it("rejects an empty name", () => {
110
+ expect(authorSchema.safeParse({ name: "" }).success).toBe(false);
111
+ });
112
+ it("tolerates a malformed url so a typo never fails a site build", () => {
113
+ expect(authorSchema.safeParse({ name: "Pedro", url: "not-a-url" }).success).toBe(true);
112
114
  });
113
115
  });
@@ -1,7 +1,17 @@
1
1
  import { z } from "zod";
2
2
 
3
+ // Mirrors @easeia/astro-content's BuildAuthor after its loader strips nulls
4
+ // to undefined. Plain strings (not z.url()) on purpose: a typo in the admin's
5
+ // author URL must not fail 12 site builds.
6
+ const authorSchema = z.object({
7
+ bio: z.string().optional(),
8
+ name: z.string().min(1),
9
+ photoUrl: z.string().optional(),
10
+ url: z.string().optional(),
11
+ });
12
+
3
13
  const postSchema = z.object({
4
- author: z.string().optional(),
14
+ author: authorSchema.optional(),
5
15
  categories: z.array(z.string()).default([]),
6
16
  cover: z.string().optional(),
7
17
  coverAlt: z.string().optional(),
@@ -29,13 +39,6 @@ const tagSchema = z.object({
29
39
  name: z.string().min(1),
30
40
  });
31
41
 
32
- const authorSchema = z.object({
33
- avatar: z.string().optional(),
34
- bio: z.string().optional(),
35
- name: z.string().min(1),
36
- url: z.url().optional(),
37
- });
38
-
39
42
  type Post = z.infer<typeof postSchema>;
40
43
  type Tag = z.infer<typeof tagSchema>;
41
44
  type Author = z.infer<typeof authorSchema>;
@@ -14,6 +14,7 @@ type Props = {
14
14
  locale?: string;
15
15
  publishedTime?: Date;
16
16
  modifiedTime?: Date;
17
+ authorName?: string;
17
18
  colorScheme?: "auto" | "light" | "dark";
18
19
  };
19
20
 
@@ -30,6 +31,7 @@ const {
30
31
  locale,
31
32
  publishedTime,
32
33
  modifiedTime,
34
+ authorName,
33
35
  colorScheme = "auto",
34
36
  } = Astro.props;
35
37
 
@@ -59,6 +61,7 @@ const colorSchemeScript = colorScheme === "auto"
59
61
  locale={locale}
60
62
  publishedTime={publishedTime}
61
63
  modifiedTime={modifiedTime}
64
+ authorName={authorName}
62
65
  />
63
66
  {rssHref && (
64
67
  <link rel="alternate" type="application/rss+xml" title={siteTitle ?? title} href={rssHref} />
@@ -11,6 +11,7 @@ type PostEntry = { id: string; data: Post };
11
11
  type Props = {
12
12
  title: string;
13
13
  description: string;
14
+ eyebrow?: string;
14
15
  posts: Array<PostEntry>;
15
16
  page?: { currentPage: number; lastPage: number; url: { prev?: string; next?: string } };
16
17
  urlForPage?: (page: number) => string;
@@ -20,18 +21,25 @@ type Props = {
20
21
  postHref?: (post: PostEntry) => string;
21
22
  };
22
23
 
23
- const { title, description, posts, page, urlForPage, locale, siteTitle, rssHref, postHref } =
24
+ const { title, description, eyebrow, posts, page, urlForPage, locale, siteTitle, rssHref, postHref } =
24
25
  Astro.props;
25
26
  ---
26
27
  <BaseLayout title={title} description={description} siteTitle={siteTitle} rssHref={rssHref}>
27
28
  <Fragment slot="header">{siteTitle && <Header siteTitle={siteTitle} />}</Fragment>
28
29
  <Fragment slot="footer">{siteTitle && <Footer siteTitle={siteTitle} />}</Fragment>
29
- <main id="main-content" tabindex="-1" class="mx-auto max-w-3xl px-4 py-10">
30
- <header class="mb-8">
31
- <h1 class="text-primary text-3xl font-bold tracking-tight">{title}</h1>
32
- <p class="text-muted-foreground mt-2">{description}</p>
30
+ <main id="main-content" tabindex="-1" class="mx-auto w-full max-w-5xl px-5 py-12 sm:px-8 sm:py-20">
31
+ <header class="mb-12 grid gap-8 border-b border-border pb-12 md:grid-cols-[1.2fr_0.8fr] md:items-end">
32
+ <div>
33
+ {eyebrow && (
34
+ <p class="text-muted-foreground font-mono text-xs font-semibold tracking-[0.16em]">{eyebrow}</p>
35
+ )}
36
+ <h1 class="text-foreground mt-4 max-w-[14ch] font-serif text-5xl leading-[0.95] font-semibold tracking-[-0.045em] sm:text-7xl">
37
+ {title}
38
+ </h1>
39
+ </div>
40
+ <p class="text-muted-foreground max-w-[38ch] text-lg text-pretty md:justify-self-end">{description}</p>
33
41
  </header>
34
- <section>
42
+ <section class="post-grid">
35
43
  {posts.map((post) => <PostCard post={post} locale={locale} postHref={postHref} />)}
36
44
  </section>
37
45
  {page && urlForPage && (
@@ -43,3 +51,41 @@ const { title, description, posts, page, urlForPage, locale, siteTitle, rssHref,
43
51
  )}
44
52
  </main>
45
53
  </BaseLayout>
54
+
55
+ <style is:global>
56
+ .post-grid {
57
+ display: grid;
58
+ gap: 1px;
59
+ overflow: hidden;
60
+ border: 1px solid var(--color-border);
61
+ background: var(--color-border);
62
+ }
63
+
64
+ .post-grid .post-card {
65
+ border: 0;
66
+ background: var(--color-background);
67
+ padding: 1.75rem;
68
+ }
69
+
70
+ .post-grid .post-card:first-child h2 {
71
+ max-width: 18ch;
72
+ font-family: var(--font-serif);
73
+ font-size: clamp(1.75rem, 4vw, 2.75rem);
74
+ line-height: 1.05;
75
+ }
76
+
77
+ @media (min-width: 48rem) {
78
+ .post-grid {
79
+ grid-template-columns: repeat(2, minmax(0, 1fr));
80
+ }
81
+
82
+ .post-grid .post-card:first-child {
83
+ grid-column: 1 / -1;
84
+ min-height: 18rem;
85
+ display: flex;
86
+ flex-direction: column;
87
+ justify-content: end;
88
+ padding: 3rem;
89
+ }
90
+ }
91
+ </style>
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { categoryLabel } from "./category-label";
4
+ import { createPostUrl } from "./post-url";
5
+
6
+ const DEFAULT = "sem-categoria";
7
+
8
+ describe("categoryLabel", () => {
9
+ it("decodes the named entities the WordPress migrations emit", () => {
10
+ expect(categoryLabel("Arte &amp; Design")).toBe("Arte & Design");
11
+ expect(categoryLabel("Casas &hellip;")).toBe("Casas \u2026");
12
+ expect(categoryLabel("&lt;tag&gt; &quot;x&quot; &rsquo;")).toBe('<tag> "x" \u2019');
13
+ expect(categoryLabel("Ano &ndash; Fim")).toBe("Ano \u2013 Fim");
14
+ });
15
+
16
+ it("matches named entities case-insensitively", () => {
17
+ expect(categoryLabel("A &AMP; B")).toBe("A & B");
18
+ });
19
+
20
+ it("decodes decimal and hexadecimal numeric entities", () => {
21
+ expect(categoryLabel("&#65;")).toBe("A");
22
+ expect(categoryLabel("&#x2013;")).toBe("\u2013");
23
+ expect(categoryLabel("&#X2013;")).toBe("\u2013");
24
+ });
25
+
26
+ it("leaves an encoded em-dash encoded in both numeric forms", () => {
27
+ expect(categoryLabel("A &#8212; B")).toBe("A &#8212; B");
28
+ expect(categoryLabel("A &#x2014; B")).toBe("A &#x2014; B");
29
+ });
30
+
31
+ it("leaves out-of-range code points alone instead of throwing", () => {
32
+ expect(() => categoryLabel("&#99999999999;")).not.toThrow();
33
+ expect(categoryLabel("&#99999999999;")).toBe("&#99999999999;");
34
+ expect(categoryLabel("&#x110000;")).toBe("&#x110000;");
35
+ });
36
+
37
+ it("does not resolve inherited Object.prototype keys", () => {
38
+ expect(categoryLabel("&constructor;")).toBe("&constructor;");
39
+ expect(categoryLabel("&toString;")).toBe("&toString;");
40
+ expect(categoryLabel("&hasOwnProperty;")).toBe("&hasOwnProperty;");
41
+ });
42
+
43
+ it("passes unknown entities and plain text through unchanged", () => {
44
+ expect(categoryLabel("&bogus;")).toBe("&bogus;");
45
+ expect(categoryLabel("Arquitetura")).toBe("Arquitetura");
46
+ expect(categoryLabel("")).toBe("");
47
+ });
48
+
49
+ it("stays display-only: decoding a name must not change the URL it resolves to", () => {
50
+ const encodedMap = { "Arte &amp; Design": "arte-e-design" };
51
+ const { postUrl } = createPostUrl(encodedMap, DEFAULT);
52
+ const post = { data: { categories: ["Arte &amp; Design"] }, id: "casa" };
53
+ expect(postUrl(post)).toBe("/arte-e-design/casa/");
54
+ expect(Object.hasOwn(encodedMap, categoryLabel("Arte &amp; Design"))).toBe(false);
55
+ });
56
+ });
@@ -0,0 +1,48 @@
1
+ const NAMED_ENTITIES = {
2
+ amp: "&",
3
+ apos: "'",
4
+ gt: ">",
5
+ hellip: "\u2026",
6
+ ldquo: "\u201c",
7
+ lsquo: "\u2018",
8
+ lt: "<",
9
+ nbsp: "\u00a0",
10
+ ndash: "\u2013",
11
+ quot: '"',
12
+ rdquo: "\u201d",
13
+ rsquo: "\u2019",
14
+ } as const;
15
+
16
+ const MAX_CODE_POINT = 1_114_111;
17
+ const EM_DASH = 8212;
18
+ const HEX_RADIX = 16;
19
+ const ENTITY_PATTERN = /&(?:#(?<decimal>\d+)|#x(?<hex>[\dA-F]+)|(?<named>[A-Z]+));/giv;
20
+
21
+ const isNamedEntity = (key: string): key is keyof typeof NAMED_ENTITIES =>
22
+ Object.hasOwn(NAMED_ENTITIES, key);
23
+
24
+ const fromCodePoint = (codePoint: number, entity: string): string =>
25
+ Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH
26
+ ? String.fromCodePoint(codePoint)
27
+ : entity;
28
+
29
+ const decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {
30
+ if (decimal !== undefined) {
31
+ return fromCodePoint(Number(decimal), entity);
32
+ }
33
+ if (hex !== undefined) {
34
+ return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
35
+ }
36
+ if (named === undefined) {
37
+ return entity;
38
+ }
39
+ const key = named.toLowerCase();
40
+ if (!isNamedEntity(key)) {
41
+ return entity;
42
+ }
43
+ return NAMED_ENTITIES[key];
44
+ };
45
+
46
+ const categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);
47
+
48
+ export { categoryLabel };
package/src/lib/index.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export { cn } from "./cn";
2
+ export { categoryLabel } from "./category-label";
2
3
  export { formatDate } from "./format-date";
3
4
  export { getRelatedPosts } from "./get-related-posts";
4
5
  export type { PostLike, PostParams, PostUrlBuilder } from "./post-url";
5
- export { categoryLabel, createPostUrl } from "./post-url";
6
+ export { createPostUrl } from "./post-url";
6
7
  export { readingTime } from "./reading-time";
7
8
  export { requireEnv } from "./require-env";
8
9
  export { slugify } from "./slugify";
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { categoryLabel, createPostUrl } from "./post-url";
3
+ import { createPostUrl } from "./post-url";
4
4
 
5
5
  const map = { "Aluguel de Espaços": "aluguel-de-espacos", Arquitetura: "arquitetura" };
6
6
  const DEFAULT = "sem-categoria";
@@ -48,53 +48,3 @@ describe("createPostUrl", () => {
48
48
  expect(postParams(post)).toEqual({ category: "arquitetura", slug: "x" });
49
49
  });
50
50
  });
51
-
52
- describe("categoryLabel", () => {
53
- it("decodes the named entities the WordPress migrations emit", () => {
54
- expect(categoryLabel("Arte &amp; Design")).toBe("Arte & Design");
55
- expect(categoryLabel("Casas &hellip;")).toBe("Casas \u2026");
56
- expect(categoryLabel("&lt;tag&gt; &quot;x&quot; &rsquo;")).toBe('<tag> "x" \u2019');
57
- expect(categoryLabel("Ano &ndash; Fim")).toBe("Ano \u2013 Fim");
58
- });
59
-
60
- it("matches named entities case-insensitively", () => {
61
- expect(categoryLabel("A &AMP; B")).toBe("A & B");
62
- });
63
-
64
- it("decodes decimal and hexadecimal numeric entities", () => {
65
- expect(categoryLabel("&#65;")).toBe("A");
66
- expect(categoryLabel("&#x2013;")).toBe("\u2013");
67
- expect(categoryLabel("&#X2013;")).toBe("\u2013");
68
- });
69
-
70
- it("leaves an encoded em-dash encoded in both numeric forms", () => {
71
- expect(categoryLabel("A &#8212; B")).toBe("A &#8212; B");
72
- expect(categoryLabel("A &#x2014; B")).toBe("A &#x2014; B");
73
- });
74
-
75
- it("leaves out-of-range code points alone instead of throwing", () => {
76
- expect(() => categoryLabel("&#99999999999;")).not.toThrow();
77
- expect(categoryLabel("&#99999999999;")).toBe("&#99999999999;");
78
- expect(categoryLabel("&#x110000;")).toBe("&#x110000;");
79
- });
80
-
81
- it("does not resolve inherited Object.prototype keys", () => {
82
- expect(categoryLabel("&constructor;")).toBe("&constructor;");
83
- expect(categoryLabel("&toString;")).toBe("&toString;");
84
- expect(categoryLabel("&hasOwnProperty;")).toBe("&hasOwnProperty;");
85
- });
86
-
87
- it("passes unknown entities and plain text through unchanged", () => {
88
- expect(categoryLabel("&bogus;")).toBe("&bogus;");
89
- expect(categoryLabel("Arquitetura")).toBe("Arquitetura");
90
- expect(categoryLabel("")).toBe("");
91
- });
92
-
93
- it("stays display-only: decoding a name must not change the URL it resolves to", () => {
94
- const encodedMap = { "Arte &amp; Design": "arte-e-design" };
95
- const { postUrl } = createPostUrl(encodedMap, DEFAULT);
96
- const post = { data: { categories: ["Arte &amp; Design"] }, id: "casa" };
97
- expect(postUrl(post)).toBe("/arte-e-design/casa/");
98
- expect(Object.hasOwn(encodedMap, categoryLabel("Arte &amp; Design"))).toBe(false);
99
- });
100
- });
@@ -49,46 +49,5 @@ const createPostUrl = (
49
49
  return { postParams, postUrl };
50
50
  };
51
51
 
52
- const NAMED_ENTITIES: Record<string, string> = {
53
- amp: "&",
54
- apos: "'",
55
- gt: ">",
56
- hellip: "\u2026",
57
- ldquo: "\u201c",
58
- lsquo: "\u2018",
59
- lt: "<",
60
- nbsp: "\u00a0",
61
- ndash: "\u2013",
62
- quot: '"',
63
- rdquo: "\u201d",
64
- rsquo: "\u2019",
65
- };
66
-
67
- const MAX_CODE_POINT = 1_114_111;
68
- const EM_DASH = 8212;
69
- const HEX_RADIX = 16;
70
- const ENTITY_PATTERN = /&(?:#(?<decimal>\d+)|#x(?<hex>[\dA-F]+)|(?<named>[A-Z]+));/giv;
71
-
72
- const fromCodePoint = (codePoint: number, entity: string): string =>
73
- Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH
74
- ? String.fromCodePoint(codePoint)
75
- : entity;
76
-
77
- const decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {
78
- if (decimal !== undefined) {
79
- return fromCodePoint(Number(decimal), entity);
80
- }
81
- if (hex !== undefined) {
82
- return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);
83
- }
84
- if (named === undefined) {
85
- return entity;
86
- }
87
- const key = named.toLowerCase();
88
- return Object.hasOwn(NAMED_ENTITIES, key) ? (NAMED_ENTITIES[key] ?? entity) : entity;
89
- };
90
-
91
- const categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);
92
-
93
52
  export type { PostLike, PostParams, PostUrlBuilder };
94
- export { categoryLabel, createPostUrl };
53
+ export { createPostUrl };
@@ -100,6 +100,7 @@
100
100
  }
101
101
  html {
102
102
  -webkit-text-size-adjust: 100%;
103
+ scrollbar-gutter: stable;
103
104
  }
104
105
  body {
105
106
  background-color: var(--color-background);
@@ -1 +0,0 @@
1
- {"version":3,"file":"lib-CBjp3x7a.js","names":[],"sources":["../src/lib/format-date.ts","../src/lib/get-related-posts.ts","../src/lib/slugify.ts","../src/lib/post-url.ts","../src/lib/reading-time.ts","../src/lib/require-env.ts","../src/lib/tag-url.ts","../src/lib/index.ts"],"sourcesContent":["const defaultOptions: Intl.DateTimeFormatOptions = {\n day: \"numeric\",\n month: \"long\",\n timeZone: \"UTC\",\n year: \"numeric\",\n};\n\nconst formatterCache = new Map<string, Intl.DateTimeFormat>();\n\nconst getFormatter = (key: string, locale: string, options: Intl.DateTimeFormatOptions) => {\n let formatter = formatterCache.get(key);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(locale, options);\n formatterCache.set(key, formatter);\n }\n return formatter;\n};\n\nconst formatDate = (date: Date, locale: string = \"en-US\", options?: Intl.DateTimeFormatOptions) => {\n if (!options) {\n return getFormatter(locale, locale, defaultOptions).format(date);\n }\n const merged = { ...defaultOptions, ...options };\n return getFormatter(`${locale}:${JSON.stringify(merged)}`, locale, merged).format(date);\n};\n\nexport { formatDate };\n","type HasTags = { data: { tags: Array<string> }; id: string };\n\nconst getRelatedPosts = <T extends HasTags>(current: T, all: Array<T>, n: number) => {\n const currentTagSet = new Set(current.data.tags);\n const scored: Array<{ post: T; score: number }> = [];\n\n for (const post of all) {\n if (post.id === current.id) {\n continue;\n }\n let score = 0;\n for (const tag of post.data.tags) {\n if (currentTagSet.has(tag)) {\n score++;\n }\n }\n scored.push({ post, score });\n }\n\n return scored\n .toSorted((x, y) => y.score - x.score)\n .slice(0, n)\n .map(({ post }) => post);\n};\n\nexport { getRelatedPosts };\n","const slugify = (input: string) =>\n input\n .normalize(\"NFD\")\n .replaceAll(/[̀-ͯ]/gv, \"\")\n .toLowerCase()\n .replaceAll(/[^a-z0-9\\s\\-]/gv, \"\")\n .trim()\n .replaceAll(/[\\s\\-]+/gv, \"-\")\n .replaceAll(/^-+|-+$/gv, \"\");\n\nexport { slugify };\n","import { slugify } from \"./slugify\";\n\ntype PostLike = {\n data: {\n categories?: Array<string>;\n seo?: { canonical_url?: string };\n };\n id: string;\n};\n\ntype PostParams = { category: string; slug: string };\n\ntype PostUrlBuilder = {\n postParams: (post: PostLike) => PostParams;\n postUrl: (post: PostLike) => string;\n};\n\nconst createPostUrl = (\n categorySlugMap: Record<string, string>,\n defaultCategory: string,\n): PostUrlBuilder => {\n const postParams = (post: PostLike): PostParams => {\n const canonical = post.data.seo?.canonical_url;\n if (canonical !== undefined && canonical !== \"\") {\n try {\n const segments = new URL(canonical).pathname.split(\"/\").filter(Boolean);\n const category = segments.at(-2);\n const slug = segments.at(-1);\n if (category !== undefined && slug !== undefined) {\n return { category, slug };\n }\n } catch {\n // Malformed canonical_url; fall through to category-derived logic.\n }\n }\n const first = post.data.categories?.at(0);\n const category =\n first !== undefined && first !== \"\"\n ? (categorySlugMap[first] ?? slugify(first))\n : defaultCategory;\n return { category, slug: post.id };\n };\n\n const postUrl = (post: PostLike): string => {\n const { category, slug } = postParams(post);\n return `/${category}/${slug}/`;\n };\n\n return { postParams, postUrl };\n};\n\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: \"&\",\n apos: \"'\",\n gt: \">\",\n hellip: \"\\u2026\",\n ldquo: \"\\u201c\",\n lsquo: \"\\u2018\",\n lt: \"<\",\n nbsp: \"\\u00a0\",\n ndash: \"\\u2013\",\n quot: '\"',\n rdquo: \"\\u201d\",\n rsquo: \"\\u2019\",\n};\n\nconst MAX_CODE_POINT = 1_114_111;\nconst EM_DASH = 8212;\nconst HEX_RADIX = 16;\nconst ENTITY_PATTERN = /&(?:#(?<decimal>\\d+)|#x(?<hex>[\\dA-F]+)|(?<named>[A-Z]+));/giv;\n\nconst fromCodePoint = (codePoint: number, entity: string): string =>\n Number.isInteger(codePoint) && codePoint <= MAX_CODE_POINT && codePoint !== EM_DASH\n ? String.fromCodePoint(codePoint)\n : entity;\n\nconst decodeEntity = (entity: string, decimal?: string, hex?: string, named?: string): string => {\n if (decimal !== undefined) {\n return fromCodePoint(Number(decimal), entity);\n }\n if (hex !== undefined) {\n return fromCodePoint(Number.parseInt(hex, HEX_RADIX), entity);\n }\n if (named === undefined) {\n return entity;\n }\n const key = named.toLowerCase();\n return Object.hasOwn(NAMED_ENTITIES, key) ? (NAMED_ENTITIES[key] ?? entity) : entity;\n};\n\nconst categoryLabel = (name: string): string => name.replaceAll(ENTITY_PATTERN, decodeEntity);\n\nexport type { PostLike, PostParams, PostUrlBuilder };\nexport { categoryLabel, createPostUrl };\n","const WORDS_PER_MINUTE = 200;\n\nconst readingTime = (input: string) => {\n const stripped = input\n .replaceAll(/<[^>]*>/gv, \"\")\n .replaceAll(/[#*_`>\\-]/gv, \" \")\n .replaceAll(/\\s+/gv, \" \")\n .trim();\n const words = stripped.length === 0 ? 0 : stripped.split(\" \").length;\n const minutes = Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n return { minutes, words };\n};\n\nexport { readingTime };\n","const requireEnv = (name: string): string => {\n const value = process.env[name];\n if (value === undefined || value === \"\") {\n throw new Error(`Missing required env var: ${name}`);\n }\n return value;\n};\n\nexport { requireEnv };\n","import { slugify } from \"./slugify\";\n\nconst DEFAULT_TAG_BASE = \"/tag\";\n\nconst tagUrl = (tag: string, base: string = DEFAULT_TAG_BASE): string =>\n `${base.replace(/\\/+$/v, \"\")}/${slugify(tag)}/`;\n\nexport { DEFAULT_TAG_BASE, tagUrl };\n","export { cn } from \"./cn\";\nexport { formatDate } from \"./format-date\";\nexport { getRelatedPosts } from \"./get-related-posts\";\nexport type { PostLike, PostParams, PostUrlBuilder } from \"./post-url\";\nexport { categoryLabel, createPostUrl } from \"./post-url\";\nexport { readingTime } from \"./reading-time\";\nexport { requireEnv } from \"./require-env\";\nexport { slugify } from \"./slugify\";\nexport { DEFAULT_TAG_BASE, tagUrl } from \"./tag-url\";\n"],"mappings":"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"}