@fifthbell/brokaw 0.1.39
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 +45 -0
- package/dist/carousels.d.ts +1 -0
- package/dist/carousels.js +65 -0
- package/dist/components/live-program/LiveProgram.d.ts +8 -0
- package/dist/components/live-program/LiveProgram.js +526 -0
- package/dist/components/live-program/assets.d.ts +14 -0
- package/dist/components/live-program/assets.js +14 -0
- package/dist/components/live-program/components/Marquee.d.ts +16 -0
- package/dist/components/live-program/components/Marquee.js +88 -0
- package/dist/components/live-program/components/MarqueeCurtain.d.ts +5 -0
- package/dist/components/live-program/components/MarqueeCurtain.js +30 -0
- package/dist/components/live-program/components/WorldClocks.d.ts +19 -0
- package/dist/components/live-program/components/WorldClocks.js +101 -0
- package/dist/components/live-program/components/slides/ArticleSlide.d.ts +14 -0
- package/dist/components/live-program/components/slides/ArticleSlide.js +22 -0
- package/dist/components/live-program/components/slides/CallsignSlide.d.ts +6 -0
- package/dist/components/live-program/components/slides/CallsignSlide.js +49 -0
- package/dist/components/live-program/components/slides/slideStyles.d.ts +1 -0
- package/dist/components/live-program/components/slides/slideStyles.js +64 -0
- package/dist/components/live-program/events.d.ts +34 -0
- package/dist/components/live-program/events.js +167 -0
- package/dist/components/live-program/hooks/useSSE.d.ts +11 -0
- package/dist/components/live-program/hooks/useSSE.js +67 -0
- package/dist/components/live-program/i18n.d.ts +4 -0
- package/dist/components/live-program/i18n.js +290 -0
- package/dist/components/live-program/segments/ArticlesSegment.d.ts +6 -0
- package/dist/components/live-program/segments/ArticlesSegment.js +160 -0
- package/dist/components/live-program/segments/EarthquakeSegment.d.ts +16 -0
- package/dist/components/live-program/segments/EarthquakeSegment.js +130 -0
- package/dist/components/live-program/segments/MarketsSegment.d.ts +12 -0
- package/dist/components/live-program/segments/MarketsSegment.js +87 -0
- package/dist/components/live-program/segments/WeatherSegment.d.ts +15 -0
- package/dist/components/live-program/segments/WeatherSegment.js +184 -0
- package/dist/components/live-program/segments/index.d.ts +6 -0
- package/dist/components/live-program/segments/index.js +6 -0
- package/dist/components/live-program/segments/types.d.ts +23 -0
- package/dist/components/live-program/segments/types.js +1 -0
- package/dist/components/live-program/segments/usePlaylistEngine.d.ts +9 -0
- package/dist/components/live-program/segments/usePlaylistEngine.js +108 -0
- package/dist/components/live-program/utils/broadcastTime.d.ts +12 -0
- package/dist/components/live-program/utils/broadcastTime.js +33 -0
- package/dist/homepage-distributor.d.ts +55 -0
- package/dist/homepage-distributor.js +68 -0
- package/dist/instagram-image-template.d.ts +8 -0
- package/dist/instagram-image-template.js +200 -0
- package/dist/outlet-config.d.ts +23 -0
- package/dist/outlet-config.js +23 -0
- package/dist/renderer.browser.d.ts +2 -0
- package/dist/renderer.browser.js +128 -0
- package/dist/renderer.core.d.ts +9 -0
- package/dist/renderer.core.js +353 -0
- package/dist/renderer.d.ts +3 -0
- package/dist/renderer.js +3 -0
- package/dist/renderer.node.d.ts +2 -0
- package/dist/renderer.node.js +71 -0
- package/dist/types/canonical-article.d.ts +247 -0
- package/dist/types/canonical-article.js +235 -0
- package/dist/utils/sofascore.d.ts +3 -0
- package/dist/utils/sofascore.js +31 -0
- package/package.json +78 -0
- package/src/partial-deps.json +52 -0
- package/src/styles/compiled.css +2 -0
- package/src/templates/layouts/404.hbs +5 -0
- package/src/templates/layouts/article-page.hbs +5 -0
- package/src/templates/layouts/category-page.hbs +5 -0
- package/src/templates/layouts/homepage.hbs +5 -0
- package/src/templates/layouts/link-in-bio.hbs +228 -0
- package/src/templates/layouts/live-story.hbs +5 -0
- package/src/templates/layouts/search-page.hbs +5 -0
- package/src/templates/partials/blocks/audio.hbs +12 -0
- package/src/templates/partials/blocks/data-table.hbs +23 -0
- package/src/templates/partials/blocks/divider.hbs +1 -0
- package/src/templates/partials/blocks/heading.hbs +9 -0
- package/src/templates/partials/blocks/image.hbs +6 -0
- package/src/templates/partials/blocks/info-box.hbs +8 -0
- package/src/templates/partials/blocks/instagram.hbs +28 -0
- package/src/templates/partials/blocks/key-points.hbs +8 -0
- package/src/templates/partials/blocks/list.hbs +13 -0
- package/src/templates/partials/blocks/live-update.hbs +24 -0
- package/src/templates/partials/blocks/pull-quote.hbs +6 -0
- package/src/templates/partials/blocks/rich-text.hbs +1 -0
- package/src/templates/partials/blocks/tiktok.hbs +15 -0
- package/src/templates/partials/blocks/x.hbs +74 -0
- package/src/templates/partials/blocks/youtube.hbs +12 -0
- package/src/templates/partials/components/article-main.hbs +159 -0
- package/src/templates/partials/components/breaking-news/live-updates-column.hbs +29 -0
- package/src/templates/partials/components/breaking-news.hbs +56 -0
- package/src/templates/partials/components/category/header.hbs +5 -0
- package/src/templates/partials/components/category/main-grid.hbs +55 -0
- package/src/templates/partials/components/category/main.hbs +7 -0
- package/src/templates/partials/components/category/more-grid.hbs +26 -0
- package/src/templates/partials/components/editorial-hero.hbs +73 -0
- package/src/templates/partials/components/headline.hbs +15 -0
- package/src/templates/partials/components/hero-editorial.hbs +1 -0
- package/src/templates/partials/components/hero.hbs +1 -0
- package/src/templates/partials/components/home/landing.hbs +111 -0
- package/src/templates/partials/components/home/main.hbs +63 -0
- package/src/templates/partials/components/home/more-stories.hbs +23 -0
- package/src/templates/partials/components/home/must-read.hbs +77 -0
- package/src/templates/partials/components/live-story/main.hbs +229 -0
- package/src/templates/partials/components/not-found/main.hbs +28 -0
- package/src/templates/partials/components/search/main.hbs +420 -0
- package/src/templates/partials/components/snack.hbs +92 -0
- package/src/templates/partials/components/spotlight-hero.hbs +59 -0
- package/src/templates/partials/components/trending.hbs +14 -0
- package/src/templates/partials/components/ui/accordion.hbs +30 -0
- package/src/templates/partials/components/ui/breadcrumb.hbs +16 -0
- package/src/templates/partials/components/ui/icon-button.hbs +19 -0
- package/src/templates/partials/components/ui/loading-spinner.hbs +27 -0
- package/src/templates/partials/components/ui/pagination.hbs +56 -0
- package/src/templates/partials/components/ui/scroll-area.hbs +12 -0
- package/src/templates/partials/components/ui/status-badge.hbs +21 -0
- package/src/templates/partials/footers/footer-full.hbs +79 -0
- package/src/templates/partials/footers/footer-minimal.hbs +5 -0
- package/src/templates/partials/headers/header-main.hbs +397 -0
- package/src/templates/partials/headers/header-minimal.hbs +16 -0
- package/src/templates/partials/nav/nav-categories.hbs +5 -0
- package/src/templates/partials/shell/doc-end.hbs +282 -0
- package/src/templates/partials/shell/doc-start-404.hbs +28 -0
- package/src/templates/partials/shell/doc-start-standard.hbs +68 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import articleLayoutHbs from './templates/layouts/article-page.hbs?raw';
|
|
2
|
+
import homepageLayoutHbs from './templates/layouts/homepage.hbs?raw';
|
|
3
|
+
import categoryLayoutHbs from './templates/layouts/category-page.hbs?raw';
|
|
4
|
+
import searchLayoutHbs from './templates/layouts/search-page.hbs?raw';
|
|
5
|
+
import notFoundLayoutHbs from './templates/layouts/404.hbs?raw';
|
|
6
|
+
import liveStoryLayoutHbs from './templates/layouts/live-story.hbs?raw';
|
|
7
|
+
import linkInBioLayoutHbs from './templates/layouts/link-in-bio.hbs?raw';
|
|
8
|
+
import headerMainHbs from './templates/partials/headers/header-main.hbs?raw';
|
|
9
|
+
import headerMinimalHbs from './templates/partials/headers/header-minimal.hbs?raw';
|
|
10
|
+
import footerFullHbs from './templates/partials/footers/footer-full.hbs?raw';
|
|
11
|
+
import footerMinimalHbs from './templates/partials/footers/footer-minimal.hbs?raw';
|
|
12
|
+
import navCategoriesHbs from './templates/partials/nav/nav-categories.hbs?raw';
|
|
13
|
+
import shellDocStartStandardHbs from './templates/partials/shell/doc-start-standard.hbs?raw';
|
|
14
|
+
import shellDocStart404Hbs from './templates/partials/shell/doc-start-404.hbs?raw';
|
|
15
|
+
import shellDocEndHbs from './templates/partials/shell/doc-end.hbs?raw';
|
|
16
|
+
import blockRichTextHbs from './templates/partials/blocks/rich-text.hbs?raw';
|
|
17
|
+
import blockHeadingHbs from './templates/partials/blocks/heading.hbs?raw';
|
|
18
|
+
import blockImageHbs from './templates/partials/blocks/image.hbs?raw';
|
|
19
|
+
import blockListHbs from './templates/partials/blocks/list.hbs?raw';
|
|
20
|
+
import blockDividerHbs from './templates/partials/blocks/divider.hbs?raw';
|
|
21
|
+
import blockInfoBoxHbs from './templates/partials/blocks/info-box.hbs?raw';
|
|
22
|
+
import blockKeyPointsHbs from './templates/partials/blocks/key-points.hbs?raw';
|
|
23
|
+
import blockDataTableHbs from './templates/partials/blocks/data-table.hbs?raw';
|
|
24
|
+
import blockLiveUpdateHbs from './templates/partials/blocks/live-update.hbs?raw';
|
|
25
|
+
import blockAudioHbs from './templates/partials/blocks/audio.hbs?raw';
|
|
26
|
+
import blockYoutubeHbs from './templates/partials/blocks/youtube.hbs?raw';
|
|
27
|
+
import blockXHbs from './templates/partials/blocks/x.hbs?raw';
|
|
28
|
+
import blockInstagramHbs from './templates/partials/blocks/instagram.hbs?raw';
|
|
29
|
+
import blockTikTokHbs from './templates/partials/blocks/tiktok.hbs?raw';
|
|
30
|
+
import blockPullQuoteHbs from './templates/partials/blocks/pull-quote.hbs?raw';
|
|
31
|
+
import componentArticleMainHbs from './templates/partials/components/article-main.hbs?raw';
|
|
32
|
+
import componentBreakingNewsHbs from './templates/partials/components/breaking-news.hbs?raw';
|
|
33
|
+
import componentBreakingNewsLiveUpdatesColumnHbs from './templates/partials/components/breaking-news/live-updates-column.hbs?raw';
|
|
34
|
+
import componentSnackHbs from './templates/partials/components/snack.hbs?raw';
|
|
35
|
+
import componentHeadlineHbs from './templates/partials/components/headline.hbs?raw';
|
|
36
|
+
import componentHeroHbs from './templates/partials/components/hero.hbs?raw';
|
|
37
|
+
import componentHeroEditorialHbs from './templates/partials/components/hero-editorial.hbs?raw';
|
|
38
|
+
import componentSpotlightHeroHbs from './templates/partials/components/spotlight-hero.hbs?raw';
|
|
39
|
+
import componentEditorialHeroHbs from './templates/partials/components/editorial-hero.hbs?raw';
|
|
40
|
+
import componentTrendingHbs from './templates/partials/components/trending.hbs?raw';
|
|
41
|
+
import componentHomeMainHbs from './templates/partials/components/home/main.hbs?raw';
|
|
42
|
+
import componentHomeLandingHbs from './templates/partials/components/home/landing.hbs?raw';
|
|
43
|
+
import componentHomeMustReadHbs from './templates/partials/components/home/must-read.hbs?raw';
|
|
44
|
+
import componentHomeMoreStoriesHbs from './templates/partials/components/home/more-stories.hbs?raw';
|
|
45
|
+
import componentCategoryMainHbs from './templates/partials/components/category/main.hbs?raw';
|
|
46
|
+
import componentCategoryHeaderHbs from './templates/partials/components/category/header.hbs?raw';
|
|
47
|
+
import componentCategoryMainGridHbs from './templates/partials/components/category/main-grid.hbs?raw';
|
|
48
|
+
import componentCategoryMoreGridHbs from './templates/partials/components/category/more-grid.hbs?raw';
|
|
49
|
+
import componentSearchMainHbs from './templates/partials/components/search/main.hbs?raw';
|
|
50
|
+
import componentNotFoundMainHbs from './templates/partials/components/not-found/main.hbs?raw';
|
|
51
|
+
import componentLiveStoryMainHbs from './templates/partials/components/live-story/main.hbs?raw';
|
|
52
|
+
import componentUiAccordionHbs from './templates/partials/components/ui/accordion.hbs?raw';
|
|
53
|
+
import componentUiBreadcrumbHbs from './templates/partials/components/ui/breadcrumb.hbs?raw';
|
|
54
|
+
import componentUiIconButtonHbs from './templates/partials/components/ui/icon-button.hbs?raw';
|
|
55
|
+
import componentUiLoadingSpinnerHbs from './templates/partials/components/ui/loading-spinner.hbs?raw';
|
|
56
|
+
import componentUiPaginationHbs from './templates/partials/components/ui/pagination.hbs?raw';
|
|
57
|
+
import componentUiScrollAreaHbs from './templates/partials/components/ui/scroll-area.hbs?raw';
|
|
58
|
+
import componentUiStatusBadgeHbs from './templates/partials/components/ui/status-badge.hbs?raw';
|
|
59
|
+
import styles from './styles/compiled.css?raw';
|
|
60
|
+
import { renderWithAssets } from './renderer.core.js';
|
|
61
|
+
const assets = {
|
|
62
|
+
layouts: {
|
|
63
|
+
'article-page': articleLayoutHbs,
|
|
64
|
+
homepage: homepageLayoutHbs,
|
|
65
|
+
'category-page': categoryLayoutHbs,
|
|
66
|
+
'search-page': searchLayoutHbs,
|
|
67
|
+
'404': notFoundLayoutHbs,
|
|
68
|
+
'live-story': liveStoryLayoutHbs,
|
|
69
|
+
'link-in-bio': linkInBioLayoutHbs
|
|
70
|
+
},
|
|
71
|
+
partials: {
|
|
72
|
+
'headers/header-main': headerMainHbs,
|
|
73
|
+
'headers/header-minimal': headerMinimalHbs,
|
|
74
|
+
'footers/footer-full': footerFullHbs,
|
|
75
|
+
'footers/footer-minimal': footerMinimalHbs,
|
|
76
|
+
'nav/nav-categories': navCategoriesHbs,
|
|
77
|
+
'shell/doc-start-standard': shellDocStartStandardHbs,
|
|
78
|
+
'shell/doc-start-404': shellDocStart404Hbs,
|
|
79
|
+
'shell/doc-end': shellDocEndHbs,
|
|
80
|
+
'blocks/rich-text': blockRichTextHbs,
|
|
81
|
+
'blocks/heading': blockHeadingHbs,
|
|
82
|
+
'blocks/image': blockImageHbs,
|
|
83
|
+
'blocks/list': blockListHbs,
|
|
84
|
+
'blocks/divider': blockDividerHbs,
|
|
85
|
+
'blocks/info-box': blockInfoBoxHbs,
|
|
86
|
+
'blocks/key-points': blockKeyPointsHbs,
|
|
87
|
+
'blocks/data-table': blockDataTableHbs,
|
|
88
|
+
'blocks/live-update': blockLiveUpdateHbs,
|
|
89
|
+
'blocks/audio': blockAudioHbs,
|
|
90
|
+
'blocks/youtube': blockYoutubeHbs,
|
|
91
|
+
'blocks/x': blockXHbs,
|
|
92
|
+
'blocks/instagram': blockInstagramHbs,
|
|
93
|
+
'blocks/tiktok': blockTikTokHbs,
|
|
94
|
+
'blocks/pull-quote': blockPullQuoteHbs,
|
|
95
|
+
'components/article-main': componentArticleMainHbs,
|
|
96
|
+
'components/breaking-news': componentBreakingNewsHbs,
|
|
97
|
+
'components/breaking-news/live-updates-column': componentBreakingNewsLiveUpdatesColumnHbs,
|
|
98
|
+
'components/snack': componentSnackHbs,
|
|
99
|
+
'components/headline': componentHeadlineHbs,
|
|
100
|
+
'components/spotlight-hero': componentSpotlightHeroHbs,
|
|
101
|
+
'components/editorial-hero': componentEditorialHeroHbs,
|
|
102
|
+
'components/hero': componentHeroHbs,
|
|
103
|
+
'components/hero-editorial': componentHeroEditorialHbs,
|
|
104
|
+
'components/trending': componentTrendingHbs,
|
|
105
|
+
'components/home/main': componentHomeMainHbs,
|
|
106
|
+
'components/home/landing': componentHomeLandingHbs,
|
|
107
|
+
'components/home/must-read': componentHomeMustReadHbs,
|
|
108
|
+
'components/home/more-stories': componentHomeMoreStoriesHbs,
|
|
109
|
+
'components/category/main': componentCategoryMainHbs,
|
|
110
|
+
'components/category/header': componentCategoryHeaderHbs,
|
|
111
|
+
'components/category/main-grid': componentCategoryMainGridHbs,
|
|
112
|
+
'components/category/more-grid': componentCategoryMoreGridHbs,
|
|
113
|
+
'components/search/main': componentSearchMainHbs,
|
|
114
|
+
'components/not-found/main': componentNotFoundMainHbs,
|
|
115
|
+
'components/live-story/main': componentLiveStoryMainHbs,
|
|
116
|
+
'components/ui/accordion': componentUiAccordionHbs,
|
|
117
|
+
'components/ui/breadcrumb': componentUiBreadcrumbHbs,
|
|
118
|
+
'components/ui/icon-button': componentUiIconButtonHbs,
|
|
119
|
+
'components/ui/loading-spinner': componentUiLoadingSpinnerHbs,
|
|
120
|
+
'components/ui/pagination': componentUiPaginationHbs,
|
|
121
|
+
'components/ui/scroll-area': componentUiScrollAreaHbs,
|
|
122
|
+
'components/ui/status-badge': componentUiStatusBadgeHbs
|
|
123
|
+
},
|
|
124
|
+
styles
|
|
125
|
+
};
|
|
126
|
+
export function render(doc) {
|
|
127
|
+
return renderWithAssets(doc, assets);
|
|
128
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CanonicalArticle } from './types/canonical-article.js';
|
|
2
|
+
export type LayoutName = CanonicalArticle['layout'];
|
|
3
|
+
export type RendererAssets = {
|
|
4
|
+
layouts: Record<LayoutName, string>;
|
|
5
|
+
partials: Record<string, string>;
|
|
6
|
+
styles: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function initializeHandlebars(assets: RendererAssets): void;
|
|
9
|
+
export declare function renderWithAssets(doc: CanonicalArticle, assets: RendererAssets): string;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import Handlebars from 'handlebars';
|
|
2
|
+
import { canonicalArticleSchema } from './types/canonical-article.js';
|
|
3
|
+
import { distributeHomepageArticles } from './homepage-distributor.js';
|
|
4
|
+
import { buildSofascoreAttackMomentumUrl, buildSofascoreMatchUrl } from './utils/sofascore.js';
|
|
5
|
+
let initialized = false;
|
|
6
|
+
const layoutCache = new Map();
|
|
7
|
+
let runtimeStyles = '';
|
|
8
|
+
const removedBlockTypes = new Set(['truthSocial', 'truthsocial', 'truth-social', 'truth_social']);
|
|
9
|
+
const siteTitlesByLanguage = {
|
|
10
|
+
en: 'fifthbell - Breaking News & Current Events',
|
|
11
|
+
es: 'fifthbell - Noticias de última hora y actualidad',
|
|
12
|
+
it: 'fifthbell - Ultime notizie e attualità'
|
|
13
|
+
};
|
|
14
|
+
function normalizePathInput(value) {
|
|
15
|
+
if (typeof value !== 'string')
|
|
16
|
+
return '';
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return '';
|
|
20
|
+
try {
|
|
21
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
22
|
+
const parsed = new URL(trimmed);
|
|
23
|
+
const normalizedAbsolute = `/${(parsed.pathname || '').replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
|
|
24
|
+
if (normalizedAbsolute !== '/' && normalizedAbsolute.endsWith('/')) {
|
|
25
|
+
return normalizedAbsolute.slice(0, -1);
|
|
26
|
+
}
|
|
27
|
+
return normalizedAbsolute || '/';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Fall back to raw string normalization below.
|
|
32
|
+
}
|
|
33
|
+
const withoutQueryOrHash = trimmed.split('#')[0].split('?')[0];
|
|
34
|
+
const normalized = `/${withoutQueryOrHash.replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
|
|
35
|
+
if (normalized !== '/' && normalized.endsWith('/')) {
|
|
36
|
+
return normalized.slice(0, -1);
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
function cleanPathSegment(value) {
|
|
41
|
+
if (typeof value !== 'string')
|
|
42
|
+
return '';
|
|
43
|
+
return value.trim().replace(/^\/+|\/+$/g, '');
|
|
44
|
+
}
|
|
45
|
+
function buildLocalePath(path, language) {
|
|
46
|
+
const normalizedLanguage = language === 'es' || language === 'it' ? language : 'en';
|
|
47
|
+
if (!path)
|
|
48
|
+
return normalizedLanguage === 'en' ? '/' : `/${normalizedLanguage}`;
|
|
49
|
+
const normalized = String(path).startsWith('/') ? String(path) : `/${path}`;
|
|
50
|
+
if (normalized.startsWith('/es/') || normalized.startsWith('/it/')) {
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
if (normalizedLanguage === 'en') {
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
return `/${normalizedLanguage}${normalized}`;
|
|
57
|
+
}
|
|
58
|
+
function resolveArticleUrl(input) {
|
|
59
|
+
const explicitUrl = normalizePathInput(input.url);
|
|
60
|
+
if (explicitUrl && explicitUrl !== '/') {
|
|
61
|
+
return buildLocalePath(explicitUrl, input.language);
|
|
62
|
+
}
|
|
63
|
+
const canonicalUrl = normalizePathInput(input.canonicalUrl);
|
|
64
|
+
if (canonicalUrl && canonicalUrl !== '/') {
|
|
65
|
+
return buildLocalePath(canonicalUrl, input.language);
|
|
66
|
+
}
|
|
67
|
+
const slugRaw = typeof input.slug === 'string' ? input.slug : '';
|
|
68
|
+
const normalizedSlugPath = normalizePathInput(slugRaw);
|
|
69
|
+
const slugSegments = normalizedSlugPath.split('/').filter(Boolean);
|
|
70
|
+
if (slugSegments.length > 1) {
|
|
71
|
+
return buildLocalePath(normalizedSlugPath, input.language);
|
|
72
|
+
}
|
|
73
|
+
const slugSegment = cleanPathSegment(slugRaw);
|
|
74
|
+
const categories = Array.isArray(input.categories) ? input.categories : [];
|
|
75
|
+
const primaryCategorySlug = cleanPathSegment(categories[0]?.slug) || cleanPathSegment(input.category?.slug);
|
|
76
|
+
if (slugSegment && primaryCategorySlug) {
|
|
77
|
+
return buildLocalePath(`/${primaryCategorySlug}/${slugSegment}`, input.language);
|
|
78
|
+
}
|
|
79
|
+
if (slugSegment) {
|
|
80
|
+
return buildLocalePath(`/${slugSegment}`, input.language);
|
|
81
|
+
}
|
|
82
|
+
return buildLocalePath('/', input.language);
|
|
83
|
+
}
|
|
84
|
+
function normalizeDocument(doc) {
|
|
85
|
+
const rawBody = doc.body;
|
|
86
|
+
if (!Array.isArray(rawBody))
|
|
87
|
+
return doc;
|
|
88
|
+
return {
|
|
89
|
+
...doc,
|
|
90
|
+
body: rawBody.filter((block) => {
|
|
91
|
+
if (!block || typeof block !== 'object')
|
|
92
|
+
return true;
|
|
93
|
+
const type = block.type;
|
|
94
|
+
return typeof type !== 'string' || !removedBlockTypes.has(type);
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function registerHelpers() {
|
|
99
|
+
Handlebars.registerHelper('eq', (a, b) => a === b);
|
|
100
|
+
Handlebars.registerHelper('slice', (items, start, end) => {
|
|
101
|
+
if (!Array.isArray(items))
|
|
102
|
+
return [];
|
|
103
|
+
return items.slice(start, end);
|
|
104
|
+
});
|
|
105
|
+
Handlebars.registerHelper('uppercase', (value) => String(value ?? '').toUpperCase());
|
|
106
|
+
Handlebars.registerHelper('coalesce', (...args) => {
|
|
107
|
+
const values = args.slice(0, -1);
|
|
108
|
+
for (const value of values) {
|
|
109
|
+
if (value === null || value === undefined)
|
|
110
|
+
continue;
|
|
111
|
+
if (typeof value === 'string' && value.trim().length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
if (Array.isArray(value) && value.length === 0)
|
|
114
|
+
continue;
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
});
|
|
119
|
+
Handlebars.registerHelper('articleUrl', (...args) => {
|
|
120
|
+
const options = args[args.length - 1];
|
|
121
|
+
const hash = options?.hash;
|
|
122
|
+
const root = options?.data?.root;
|
|
123
|
+
return resolveArticleUrl({
|
|
124
|
+
url: hash?.url,
|
|
125
|
+
canonicalUrl: hash?.canonicalUrl,
|
|
126
|
+
slug: hash?.slug,
|
|
127
|
+
categories: hash?.categories,
|
|
128
|
+
category: hash?.category,
|
|
129
|
+
language: hash?.language ?? root?.language
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
Handlebars.registerHelper('formatDate', (isoString) => {
|
|
133
|
+
if (!isoString)
|
|
134
|
+
return '';
|
|
135
|
+
try {
|
|
136
|
+
const date = new Date(isoString);
|
|
137
|
+
const now = new Date();
|
|
138
|
+
const isWithin24h = now.getTime() - date.getTime() < 24 * 60 * 60 * 1000;
|
|
139
|
+
if (isWithin24h) {
|
|
140
|
+
return date.toLocaleTimeString('en-US', {
|
|
141
|
+
hour: 'numeric',
|
|
142
|
+
minute: '2-digit',
|
|
143
|
+
hour12: true,
|
|
144
|
+
timeZone: 'America/New_York'
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return date.toLocaleDateString('en-US', {
|
|
149
|
+
year: 'numeric',
|
|
150
|
+
month: 'long',
|
|
151
|
+
day: 'numeric',
|
|
152
|
+
timeZone: 'America/New_York'
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return isoString;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
Handlebars.registerHelper('xStatusUrl', (url) => {
|
|
161
|
+
if (!url)
|
|
162
|
+
return '';
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(url);
|
|
165
|
+
if (parsed.hostname === 'x.com' || parsed.hostname === 'www.x.com') {
|
|
166
|
+
return `https://twitter.com${parsed.pathname}`;
|
|
167
|
+
}
|
|
168
|
+
return url;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return url;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
Handlebars.registerHelper('xEmbedUrl', (url) => {
|
|
175
|
+
if (!url)
|
|
176
|
+
return '';
|
|
177
|
+
const buildEmbedUrl = (tweetId) => `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}&dnt=true`;
|
|
178
|
+
const idFromRaw = url.match(/status\/(\d+)/)?.[1];
|
|
179
|
+
try {
|
|
180
|
+
const parsed = new URL(url);
|
|
181
|
+
const host = parsed.hostname.replace(/^www\./, '');
|
|
182
|
+
if (host === 'x.com' || host === 'twitter.com') {
|
|
183
|
+
const tweetId = parsed.pathname.match(/\/[^/]+\/status\/(\d+)/)?.[1];
|
|
184
|
+
if (tweetId)
|
|
185
|
+
return buildEmbedUrl(tweetId);
|
|
186
|
+
}
|
|
187
|
+
return idFromRaw ? buildEmbedUrl(idFromRaw) : '';
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return idFromRaw ? buildEmbedUrl(idFromRaw) : '';
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
Handlebars.registerHelper('xTweetId', (url) => {
|
|
194
|
+
if (!url)
|
|
195
|
+
return '';
|
|
196
|
+
const idFromRaw = url.match(/status\/(\d+)/)?.[1];
|
|
197
|
+
if (idFromRaw)
|
|
198
|
+
return idFromRaw;
|
|
199
|
+
try {
|
|
200
|
+
const parsed = new URL(url);
|
|
201
|
+
const host = parsed.hostname.replace(/^www\./, '');
|
|
202
|
+
if (host === 'x.com' || host === 'twitter.com') {
|
|
203
|
+
return parsed.pathname.match(/\/[^/]+\/status\/(\d+)/)?.[1] ?? '';
|
|
204
|
+
}
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Handlebars.registerHelper('instagramEmbedUrl', (url) => {
|
|
212
|
+
if (!url)
|
|
213
|
+
return '';
|
|
214
|
+
const normalized = url.endsWith('/') ? url : `${url}/`;
|
|
215
|
+
return `${normalized}embed/`;
|
|
216
|
+
});
|
|
217
|
+
Handlebars.registerHelper('tiktokEmbedUrl', (url) => {
|
|
218
|
+
if (!url)
|
|
219
|
+
return '';
|
|
220
|
+
const match = url.match(/\/video\/(\d+)/);
|
|
221
|
+
if (!match)
|
|
222
|
+
return url;
|
|
223
|
+
return `https://www.tiktok.com/embed/v2/${match[1]}`;
|
|
224
|
+
});
|
|
225
|
+
Handlebars.registerHelper('sofascoreWidgetUrl', (id) => buildSofascoreAttackMomentumUrl(id));
|
|
226
|
+
Handlebars.registerHelper('sofascoreMatchUrl', (id) => buildSofascoreMatchUrl(id));
|
|
227
|
+
Handlebars.registerHelper('jsonString', (value) => {
|
|
228
|
+
if (value === undefined)
|
|
229
|
+
return 'null';
|
|
230
|
+
return JSON.stringify(value);
|
|
231
|
+
});
|
|
232
|
+
Handlebars.registerHelper('resolveHeadTitle', (doc) => {
|
|
233
|
+
if (!doc || typeof doc !== 'object')
|
|
234
|
+
return 'fifthbell';
|
|
235
|
+
const page = doc;
|
|
236
|
+
if (page.layout === 'homepage') {
|
|
237
|
+
const language = page.language === 'es' || page.language === 'it' ? page.language : 'en';
|
|
238
|
+
return siteTitlesByLanguage[language];
|
|
239
|
+
}
|
|
240
|
+
if (page.layout === 'category-page') {
|
|
241
|
+
const categoryName = page.categories?.[0]?.name?.trim();
|
|
242
|
+
const baseTitle = categoryName || page.title?.trim() || 'Category';
|
|
243
|
+
return `${baseTitle} | fifthbell`;
|
|
244
|
+
}
|
|
245
|
+
if (page.layout === 'search-page') {
|
|
246
|
+
return 'Search | fifthbell';
|
|
247
|
+
}
|
|
248
|
+
if (page.layout === 'article-page') {
|
|
249
|
+
const baseTitle = page.title?.trim() || 'Article';
|
|
250
|
+
return `${baseTitle} | fifthbell`;
|
|
251
|
+
}
|
|
252
|
+
if (page.layout === '404') {
|
|
253
|
+
return '404 - Page Not Found | fifthbell';
|
|
254
|
+
}
|
|
255
|
+
if (page.layout === 'live-story') {
|
|
256
|
+
const baseTitle = page.title?.trim() || 'Live Story';
|
|
257
|
+
return `${baseTitle} | fifthbell`;
|
|
258
|
+
}
|
|
259
|
+
if (page.layout === 'link-in-bio') {
|
|
260
|
+
const baseTitle = page.title?.trim() || 'Top Stories';
|
|
261
|
+
return `${baseTitle} | fifthbell`;
|
|
262
|
+
}
|
|
263
|
+
const seoTitle = page.seo?.metaTitle?.trim();
|
|
264
|
+
if (seoTitle)
|
|
265
|
+
return seoTitle;
|
|
266
|
+
const baseTitle = page.title?.trim() || 'fifthbell';
|
|
267
|
+
return `${baseTitle} | fifthbell`;
|
|
268
|
+
});
|
|
269
|
+
Handlebars.registerHelper('socialImageUrl', (value) => {
|
|
270
|
+
if (typeof value !== 'string')
|
|
271
|
+
return '';
|
|
272
|
+
const raw = value.trim();
|
|
273
|
+
if (!raw)
|
|
274
|
+
return '';
|
|
275
|
+
const normalizePath = (pathname) => pathname.replace(/\.avif$/i, '.jpg');
|
|
276
|
+
try {
|
|
277
|
+
const parsed = new URL(raw);
|
|
278
|
+
parsed.pathname = normalizePath(parsed.pathname);
|
|
279
|
+
return parsed.toString();
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Support relative URLs in template data.
|
|
283
|
+
return normalizePath(raw);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function registerPartials(partials) {
|
|
288
|
+
for (const [name, template] of Object.entries(partials)) {
|
|
289
|
+
Handlebars.registerPartial(name, template);
|
|
290
|
+
}
|
|
291
|
+
const bodyAliases = {
|
|
292
|
+
richText: 'blocks/rich-text',
|
|
293
|
+
heading: 'blocks/heading',
|
|
294
|
+
image: 'blocks/image',
|
|
295
|
+
list: 'blocks/list',
|
|
296
|
+
divider: 'blocks/divider',
|
|
297
|
+
infoBox: 'blocks/info-box',
|
|
298
|
+
keyPoints: 'blocks/key-points',
|
|
299
|
+
dataTable: 'blocks/data-table',
|
|
300
|
+
liveUpdate: 'blocks/live-update',
|
|
301
|
+
audio: 'blocks/audio',
|
|
302
|
+
youtube: 'blocks/youtube',
|
|
303
|
+
x: 'blocks/x',
|
|
304
|
+
instagram: 'blocks/instagram',
|
|
305
|
+
tiktok: 'blocks/tiktok',
|
|
306
|
+
pullQuote: 'blocks/pull-quote'
|
|
307
|
+
};
|
|
308
|
+
for (const [alias, partialName] of Object.entries(bodyAliases)) {
|
|
309
|
+
const source = partials[partialName];
|
|
310
|
+
if (source) {
|
|
311
|
+
Handlebars.registerPartial(alias, source);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function compileLayouts(layouts) {
|
|
316
|
+
for (const [name, source] of Object.entries(layouts)) {
|
|
317
|
+
layoutCache.set(name, Handlebars.compile(source));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
export function initializeHandlebars(assets) {
|
|
321
|
+
if (initialized)
|
|
322
|
+
return;
|
|
323
|
+
registerHelpers();
|
|
324
|
+
registerPartials(assets.partials);
|
|
325
|
+
compileLayouts(assets.layouts);
|
|
326
|
+
runtimeStyles = assets.styles;
|
|
327
|
+
initialized = true;
|
|
328
|
+
}
|
|
329
|
+
export function renderWithAssets(doc, assets) {
|
|
330
|
+
if (!initialized) {
|
|
331
|
+
initializeHandlebars(assets);
|
|
332
|
+
}
|
|
333
|
+
const requestedLayout = doc.layout;
|
|
334
|
+
if (!requestedLayout || !layoutCache.has(requestedLayout)) {
|
|
335
|
+
throw new Error(`Unknown layout "${requestedLayout ?? 'undefined'}". Expected one of: article-page, homepage, category-page, search-page, 404, live-story, link-in-bio`);
|
|
336
|
+
}
|
|
337
|
+
const parsed = canonicalArticleSchema.parse(normalizeDocument(doc));
|
|
338
|
+
const template = layoutCache.get(parsed.layout);
|
|
339
|
+
if (!template) {
|
|
340
|
+
throw new Error(`Layout template missing for \"${parsed.layout}\"`);
|
|
341
|
+
}
|
|
342
|
+
// Build the template context, enriching homepage renders with pre-distributed
|
|
343
|
+
// article slots so templates do not need to perform index arithmetic.
|
|
344
|
+
const docExtra = doc;
|
|
345
|
+
const showBreakingNews = parsed.layout === 'homepage' && Boolean(parsed.breakingNews) && docExtra['showBreakingNews'] !== false;
|
|
346
|
+
const homepageSlots = parsed.layout === 'homepage' ? distributeHomepageArticles(parsed.articles ?? [], new Date(), showBreakingNews) : undefined;
|
|
347
|
+
return template({
|
|
348
|
+
...parsed,
|
|
349
|
+
...(homepageSlots !== undefined ? { homepageSlots } : {}),
|
|
350
|
+
styles: runtimeStyles,
|
|
351
|
+
logoLink: parsed.language === 'en' ? '/' : `/${parsed.language}`
|
|
352
|
+
});
|
|
353
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { renderWithAssets } from './renderer.core.js';
|
|
5
|
+
const layoutFiles = {
|
|
6
|
+
'article-page': 'article-page.hbs',
|
|
7
|
+
homepage: 'homepage.hbs',
|
|
8
|
+
'category-page': 'category-page.hbs',
|
|
9
|
+
'search-page': 'search-page.hbs',
|
|
10
|
+
'404': '404.hbs',
|
|
11
|
+
'live-story': 'live-story.hbs',
|
|
12
|
+
'link-in-bio': 'link-in-bio.hbs'
|
|
13
|
+
};
|
|
14
|
+
function getPaths() {
|
|
15
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
16
|
+
const currentDir = path.dirname(currentFile);
|
|
17
|
+
const projectRoot = path.resolve(currentDir, '..');
|
|
18
|
+
return {
|
|
19
|
+
layoutsDir: path.join(projectRoot, 'src', 'templates', 'layouts'),
|
|
20
|
+
partialsDir: path.join(projectRoot, 'src', 'templates', 'partials'),
|
|
21
|
+
compiledStylesPath: path.join(projectRoot, 'src', 'styles', 'compiled.css')
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function readPartialsRecursively(dir, root, partials) {
|
|
25
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
readPartialsRecursively(fullPath, root, partials);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!entry.isFile() || !entry.name.endsWith('.hbs')) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const partialName = path
|
|
36
|
+
.relative(root, fullPath)
|
|
37
|
+
.replace(/\\/g, '/')
|
|
38
|
+
.replace(/\.hbs$/, '');
|
|
39
|
+
partials[partialName] = fs.readFileSync(fullPath, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function loadAssetsFromDisk() {
|
|
43
|
+
const { layoutsDir, partialsDir, compiledStylesPath } = getPaths();
|
|
44
|
+
if (!fs.existsSync(compiledStylesPath)) {
|
|
45
|
+
throw new Error('compiled.css not found — run npm run build:css first');
|
|
46
|
+
}
|
|
47
|
+
const layouts = Object.fromEntries(Object.entries(layoutFiles).map(([layout, fileName]) => {
|
|
48
|
+
const fullPath = path.join(layoutsDir, fileName);
|
|
49
|
+
if (!fs.existsSync(fullPath)) {
|
|
50
|
+
throw new Error(`Layout template missing for \"${layout}\": ${fullPath}`);
|
|
51
|
+
}
|
|
52
|
+
return [layout, fs.readFileSync(fullPath, 'utf8')];
|
|
53
|
+
}));
|
|
54
|
+
const partials = {};
|
|
55
|
+
readPartialsRecursively(partialsDir, partialsDir, partials);
|
|
56
|
+
return {
|
|
57
|
+
layouts,
|
|
58
|
+
partials,
|
|
59
|
+
styles: fs.readFileSync(compiledStylesPath, 'utf8')
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let cachedAssets = null;
|
|
63
|
+
function getAssets() {
|
|
64
|
+
if (!cachedAssets) {
|
|
65
|
+
cachedAssets = loadAssetsFromDisk();
|
|
66
|
+
}
|
|
67
|
+
return cachedAssets;
|
|
68
|
+
}
|
|
69
|
+
export function render(doc) {
|
|
70
|
+
return renderWithAssets(doc, getAssets());
|
|
71
|
+
}
|