Skip to content

Commit 88bf52e

Browse files
feat: magazine and news database (#209)
* feat: prepare structured press articles * feat: add versatile article handling * feat: update styling * fix: article og generator * chore: update error log * chore: bump version * chore: add caching * feat: update sitemap generator * chore: remove comment
1 parent 281708b commit 88bf52e

18 files changed

+708
-448
lines changed
Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,107 @@
1-
import { BASE_URL } from "../modules/app";
1+
import { slog } from "../modules/observability.server";
2+
import { articleSlugSchema, PressArticleContentSection } from "./article.shape";
3+
import { supabase } from "./supabase.server";
4+
import { Enums, Tables } from "./supabase.types.generated";
25

36
export class ArticleService {
47
static async checkNewsArticleExists(
58
articleSlug: string,
6-
requestUrl?: string,
9+
articleType?: Enums<"press_article_type"> | (string & {}),
710
): Promise<boolean> {
8-
const url = `${requestUrl || BASE_URL}/press/news/${encodeURIComponent(articleSlug)}`;
9-
const res = await fetch(url, { method: "HEAD" });
10-
return res.ok;
11+
articleSlugSchema.parse(articleSlug);
12+
13+
const query = supabase
14+
.from("press_articles")
15+
.select("slug", { head: true, count: "exact" })
16+
.eq("slug", articleSlug);
17+
18+
if (articleType) {
19+
query.eq("type", articleType);
20+
}
21+
22+
const { count, error } = await query.maybeSingle();
23+
24+
if (error) {
25+
slog.error("Error checking if news article exists", { error });
26+
return false;
27+
}
28+
29+
return count === 1;
30+
}
31+
32+
static async getArticleBySlug(
33+
articleSlug: string,
34+
articleType?: Enums<"press_article_type"> | (string & {}),
35+
) {
36+
articleSlugSchema.parse(articleSlug);
37+
38+
const query = supabase
39+
.from("press_articles")
40+
.select("*, authors:press_authors!inner(*)")
41+
.eq("slug", articleSlug);
42+
43+
if (articleType) {
44+
query.eq("type", articleType);
45+
}
46+
47+
const { data, error } = await query.maybeSingle();
48+
49+
if (error || !data) {
50+
slog.error("Error getting news article by slug", {
51+
articleSlug,
52+
articleType,
53+
error,
54+
});
55+
return null;
56+
}
57+
58+
return {
59+
...data,
60+
authors: data.authors as Tables<"press_authors">[],
61+
sections: data.sections as PressArticleContentSection[],
62+
};
63+
}
64+
65+
static async getAllArticlePreviews(
66+
articleType: Enums<"press_article_type"> | (string & {}),
67+
) {
68+
const query = supabase
69+
.from("press_articles")
70+
.select(
71+
[
72+
"slug",
73+
"title",
74+
"subline",
75+
"created_at",
76+
"categories",
77+
"synopsis_html",
78+
"type",
79+
].join(","),
80+
)
81+
.order("created_at", { ascending: false });
82+
83+
if (articleType) {
84+
query.eq("type", articleType);
85+
}
86+
87+
const { data, error } = await query;
88+
89+
if (error) {
90+
slog.error("Error getting all news articles", { error });
91+
return [];
92+
}
93+
94+
return data as unknown as Array<
95+
Pick<
96+
Tables<"press_articles">,
97+
| "slug"
98+
| "title"
99+
| "subline"
100+
| "created_at"
101+
| "categories"
102+
| "synopsis_html"
103+
| "type"
104+
>
105+
>;
11106
}
12107
}

web/app/data/article.shape.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
11
import { z } from "zod";
22

3-
export const articleSlugSchema = z.string().min(1).max(100);
3+
export const articleSlugSchema = z.string().min(1).max(255);
4+
5+
export type PressArticleContentSection = {
6+
headline: string;
7+
fragment: string;
8+
body: PressArticleContentBody;
9+
};
10+
11+
export type PressArticleContentBody = Array<
12+
| {
13+
type: "html";
14+
value: string;
15+
}
16+
| {
17+
type: "image";
18+
value: PressArticleContentBodyImage;
19+
}
20+
>;
21+
22+
export type PressArticleContentBodyImage = {
23+
src: string;
24+
caption: string;
25+
};

web/app/data/supabase.types.generated.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,102 @@ export type Database = {
381381
},
382382
];
383383
};
384+
press_article_authors: {
385+
Row: {
386+
author_slug: string;
387+
press_article_slug: string;
388+
};
389+
Insert: {
390+
author_slug: string;
391+
press_article_slug: string;
392+
};
393+
Update: {
394+
author_slug?: string;
395+
press_article_slug?: string;
396+
};
397+
Relationships: [
398+
{
399+
foreignKeyName: "press_article_authors_author_slug_fkey";
400+
columns: ["author_slug"];
401+
referencedRelation: "press_authors";
402+
referencedColumns: ["slug"];
403+
},
404+
{
405+
foreignKeyName: "press_article_authors_press_article_slug_fkey";
406+
columns: ["press_article_slug"];
407+
referencedRelation: "press_articles";
408+
referencedColumns: ["slug"];
409+
},
410+
];
411+
};
412+
press_articles: {
413+
Row: {
414+
categories: Database["public"]["Enums"]["press_article_category"][];
415+
created_at: string;
416+
sections: Json;
417+
slug: string;
418+
subline: string | null;
419+
synopsis_html: string;
420+
title: string;
421+
type: Database["public"]["Enums"]["press_article_type"];
422+
updated_at: string | null;
423+
};
424+
Insert: {
425+
categories: Database["public"]["Enums"]["press_article_category"][];
426+
created_at: string;
427+
sections: Json;
428+
slug: string;
429+
subline?: string | null;
430+
synopsis_html: string;
431+
title: string;
432+
type: Database["public"]["Enums"]["press_article_type"];
433+
updated_at?: string | null;
434+
};
435+
Update: {
436+
categories?: Database["public"]["Enums"]["press_article_category"][];
437+
created_at?: string;
438+
sections?: Json;
439+
slug?: string;
440+
subline?: string | null;
441+
synopsis_html?: string;
442+
title?: string;
443+
type?: Database["public"]["Enums"]["press_article_type"];
444+
updated_at?: string | null;
445+
};
446+
Relationships: [];
447+
};
448+
press_authors: {
449+
Row: {
450+
name: string;
451+
slug: string;
452+
};
453+
Insert: {
454+
name: string;
455+
slug: string;
456+
};
457+
Update: {
458+
name?: string;
459+
slug?: string;
460+
};
461+
Relationships: [];
462+
};
384463
};
385464
Views: {
386465
[_ in never]: never;
387466
};
388467
Functions: {
468+
dmetaphone: {
469+
Args: {
470+
"": string;
471+
};
472+
Returns: string;
473+
};
474+
dmetaphone_alt: {
475+
Args: {
476+
"": string;
477+
};
478+
Returns: string;
479+
};
389480
find_closest_authors: {
390481
Args: {
391482
search_term: string;
@@ -408,6 +499,64 @@ export type Database = {
408499
levenshtein_distance: number;
409500
}[];
410501
};
502+
gtrgm_compress: {
503+
Args: {
504+
"": unknown;
505+
};
506+
Returns: unknown;
507+
};
508+
gtrgm_decompress: {
509+
Args: {
510+
"": unknown;
511+
};
512+
Returns: unknown;
513+
};
514+
gtrgm_in: {
515+
Args: {
516+
"": unknown;
517+
};
518+
Returns: unknown;
519+
};
520+
gtrgm_options: {
521+
Args: {
522+
"": unknown;
523+
};
524+
Returns: undefined;
525+
};
526+
gtrgm_out: {
527+
Args: {
528+
"": unknown;
529+
};
530+
Returns: unknown;
531+
};
532+
set_limit: {
533+
Args: {
534+
"": number;
535+
};
536+
Returns: number;
537+
};
538+
show_limit: {
539+
Args: Record<PropertyKey, never>;
540+
Returns: number;
541+
};
542+
show_trgm: {
543+
Args: {
544+
"": string;
545+
};
546+
Returns: string[];
547+
};
548+
soundex: {
549+
Args: {
550+
"": string;
551+
};
552+
Returns: string;
553+
};
554+
text_soundex: {
555+
Args: {
556+
"": string;
557+
};
558+
Returns: string;
559+
};
411560
};
412561
Enums: {
413562
ai_model_modality:
@@ -429,6 +578,8 @@ export type Database = {
429578
| "reverse_suggests"
430579
| "reverse_enhances"
431580
| "reverse_linking_to";
581+
press_article_category: "general" | "announcement";
582+
press_article_type: "news" | "magazine";
432583
};
433584
CompositeTypes: {
434585
[_ in never]: never;

0 commit comments

Comments
 (0)