shabbat-gate 0.1.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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/botPattern.d.ts +8 -0
- package/dist/botPattern.js +10 -0
- package/dist/hebcal.d.ts +29 -0
- package/dist/hebcal.js +68 -0
- package/dist/holdingPage.d.ts +7 -0
- package/dist/holdingPage.js +38 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +81 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eyal Meshulam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# shabbat-gate
|
|
2
|
+
|
|
3
|
+
Cloudflare Pages / Workers middleware that automatically closes a site to human visitors
|
|
4
|
+
during Shabbat and major Jewish holidays (Israel-observance rules) - while always letting
|
|
5
|
+
search engines and AI crawlers through, so SEO stays unaffected.
|
|
6
|
+
|
|
7
|
+
## Why
|
|
8
|
+
|
|
9
|
+
- **Israel single-day Yom Tov, not diaspora 2-day.** The holiday calendar is fetched from
|
|
10
|
+
Hebcal's free public API with `i=on`, which is critical - without it you'd get the diaspora
|
|
11
|
+
reckoning (an extra blocked day) instead of the correct single-day Yom Tov used in Israel.
|
|
12
|
+
- **Bots always get through.** A broad, case-insensitive user-agent allowlist (Googlebot,
|
|
13
|
+
Bingbot, GPTBot, ClaudeBot, and many others) is checked first, before any other logic runs.
|
|
14
|
+
The gate only ever affects human visitors - crawlers and indexers see the real site 24/7, so
|
|
15
|
+
ranking and AI-search visibility are never impacted by the site being "closed."
|
|
16
|
+
- **Fails open.** Any error (network failure, bad API response, whatever) falls through to the
|
|
17
|
+
real site rather than showing an error page. An accidental block on a regular Tuesday would be
|
|
18
|
+
a real, visible bug; an occasional missed block during a rare error is a minor, invisible one.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install shabbat-gate
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
In a Cloudflare Pages project, add `functions/_middleware.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { createShabbatGate } from 'shabbat-gate';
|
|
32
|
+
|
|
33
|
+
const gate = createShabbatGate({ siteName: 'My Site' });
|
|
34
|
+
|
|
35
|
+
export const onRequest: PagesFunction = (context) => gate(context);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Config
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
export interface ShabbatGateConfig {
|
|
42
|
+
siteName: string;
|
|
43
|
+
|
|
44
|
+
/** Decimal lat/long for zmanim. Both default to Jerusalem (31.7683, 35.2137) if
|
|
45
|
+
* omitted - a fine single reference point for all of Israel at this granularity. */
|
|
46
|
+
latitude?: number;
|
|
47
|
+
longitude?: number;
|
|
48
|
+
|
|
49
|
+
/** Query param name + required value that bypasses the gate entirely, so the site
|
|
50
|
+
* owner can preview/test on any day. Keep the value non-guessable - this is a
|
|
51
|
+
* testing convenience, not real auth. */
|
|
52
|
+
bypassParam?: string;
|
|
53
|
+
bypassValue?: string;
|
|
54
|
+
|
|
55
|
+
/** Optional custom holding-page renderer. Defaults to a Hebrew, mobile-responsive
|
|
56
|
+
* page showing siteName and when the site reopens. */
|
|
57
|
+
renderHoldingPage?: (ctx: { siteName: string; reasonLabel: string; untilLabel: string }) => string;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Full example:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createShabbatGate } from 'shabbat-gate';
|
|
65
|
+
|
|
66
|
+
const gate = createShabbatGate({
|
|
67
|
+
siteName: 'tehila·games',
|
|
68
|
+
latitude: 31.7683,
|
|
69
|
+
longitude: 35.2137,
|
|
70
|
+
bypassParam: 'preview',
|
|
71
|
+
bypassValue: 'letmein-9f3a7c',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export const onRequest: PagesFunction = (context) => gate(context);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## How it works
|
|
78
|
+
|
|
79
|
+
1. Bot check (allowlist regex on the `user-agent` header) - matches pass straight through.
|
|
80
|
+
2. Bypass check - if the bypass query param + value match, pass straight through.
|
|
81
|
+
3. Fetch (with ~24h caching via the Workers Cache API) the merged list of Shabbat and major
|
|
82
|
+
holiday windows from Hebcal, ~45 days into the future.
|
|
83
|
+
4. If the current time falls inside a window, serve the holding page (HTTP 200). Otherwise let
|
|
84
|
+
the real site through.
|
|
85
|
+
5. Any error along the way falls through to the real site.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad, case-insensitive allowlist for search engine and AI crawlers.
|
|
3
|
+
* Matches let a request through unconditionally, before any gate logic runs.
|
|
4
|
+
* Erring toward "let more things through" is the safe direction here, since
|
|
5
|
+
* the whole point of this list is protecting SEO/crawlability.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BOT_PATTERN: RegExp;
|
|
8
|
+
export declare function isBot(userAgent: string): boolean;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broad, case-insensitive allowlist for search engine and AI crawlers.
|
|
3
|
+
* Matches let a request through unconditionally, before any gate logic runs.
|
|
4
|
+
* Erring toward "let more things through" is the safe direction here, since
|
|
5
|
+
* the whole point of this list is protecting SEO/crawlability.
|
|
6
|
+
*/
|
|
7
|
+
export const BOT_PATTERN = /bot|crawl|spider|slurp|googlebot|google-inspectiontool|adsbot-google|bingbot|duckduckbot|baiduspider|yandex|facebookexternalhit|twitterbot|linkedinbot|whatsapp|telegrambot|discordbot|applebot|gptbot|chatgpt-user|oai-searchbot|ccbot|claudebot|claude-web|anthropic-ai|perplexitybot|google-extended|bytespider|semrushbot|ahrefsbot|mj12bot|petalbot/i;
|
|
8
|
+
export function isBot(userAgent) {
|
|
9
|
+
return BOT_PATTERN.test(userAgent);
|
|
10
|
+
}
|
package/dist/hebcal.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface Window {
|
|
2
|
+
start: number;
|
|
3
|
+
end: number;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
interface HebcalItem {
|
|
7
|
+
title: string;
|
|
8
|
+
hebrew?: string;
|
|
9
|
+
date: string;
|
|
10
|
+
category: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Pairs candles/havdalah events into continuous windows. Multi-day holidays
|
|
14
|
+
* (e.g. Rosh Hashana) emit two "candles" events but only one "havdalah" at the
|
|
15
|
+
* very end, so candles cannot simply be paired 1:1 with the next havdalah.
|
|
16
|
+
* Instead: open a window on the first candles seen while none is open, ignore
|
|
17
|
+
* further candles while one is open, and close on the next havdalah.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pairWindows(items: HebcalItem[]): Window[];
|
|
20
|
+
/**
|
|
21
|
+
* Fetches and merges Shabbat + major-holiday (Israel single-day Yom Tov mode)
|
|
22
|
+
* windows for the next ~45 days from Hebcal's free public JSON API.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fetchWindows(latitude: number, longitude: number): Promise<Window[]>;
|
|
25
|
+
/** Pure function: is `now` inside any of the given windows? */
|
|
26
|
+
export declare function isBlocked(windows: Window[], now: number): boolean;
|
|
27
|
+
/** Pure function: the window covering `now`, if any. */
|
|
28
|
+
export declare function findActiveWindow(windows: Window[], now: number): Window | undefined;
|
|
29
|
+
export {};
|
package/dist/hebcal.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const HEBCAL_JERUSALEM_GEONAME_ID = 281184;
|
|
2
|
+
/**
|
|
3
|
+
* Pairs candles/havdalah events into continuous windows. Multi-day holidays
|
|
4
|
+
* (e.g. Rosh Hashana) emit two "candles" events but only one "havdalah" at the
|
|
5
|
+
* very end, so candles cannot simply be paired 1:1 with the next havdalah.
|
|
6
|
+
* Instead: open a window on the first candles seen while none is open, ignore
|
|
7
|
+
* further candles while one is open, and close on the next havdalah.
|
|
8
|
+
*/
|
|
9
|
+
export function pairWindows(items) {
|
|
10
|
+
const windows = [];
|
|
11
|
+
let openStart = null;
|
|
12
|
+
let openLabel = '';
|
|
13
|
+
for (const item of items) {
|
|
14
|
+
if (item.category === 'candles') {
|
|
15
|
+
if (openStart === null) {
|
|
16
|
+
openStart = new Date(item.date).getTime();
|
|
17
|
+
openLabel = item.hebrew ?? item.title;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (item.category === 'havdalah' && openStart !== null) {
|
|
21
|
+
windows.push({ start: openStart, end: new Date(item.date).getTime(), label: openLabel });
|
|
22
|
+
openStart = null;
|
|
23
|
+
openLabel = '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return windows;
|
|
27
|
+
}
|
|
28
|
+
function toISODate(date) {
|
|
29
|
+
return date.toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fetches and merges Shabbat + major-holiday (Israel single-day Yom Tov mode)
|
|
33
|
+
* windows for the next ~45 days from Hebcal's free public JSON API.
|
|
34
|
+
*/
|
|
35
|
+
export async function fetchWindows(latitude, longitude) {
|
|
36
|
+
const start = new Date();
|
|
37
|
+
const end = new Date(start.getTime() + 45 * 24 * 60 * 60 * 1000);
|
|
38
|
+
const startParam = toISODate(start);
|
|
39
|
+
const endParam = toISODate(end);
|
|
40
|
+
const shabbatUrl = `https://www.hebcal.com/shabbat?cfg=json&latitude=${latitude}&longitude=${longitude}` +
|
|
41
|
+
`&tzid=Asia/Jerusalem&M=on&start=${startParam}&end=${endParam}`;
|
|
42
|
+
// i=on = Israel single-day Yom Tov reckoning (not diaspora 2-day).
|
|
43
|
+
// c=on = attach candles/havdalah entries to holidays, not just bare dates.
|
|
44
|
+
// maj=on + everything else off = only real work-restricted Yom Tov days.
|
|
45
|
+
const holidayUrl = `https://www.hebcal.com/hebcal?cfg=json&v=1&maj=on&min=off&mod=off&nx=off&mf=off&ss=off` +
|
|
46
|
+
`&c=on&i=on&geonameid=${HEBCAL_JERUSALEM_GEONAME_ID}&start=${startParam}&end=${endParam}`;
|
|
47
|
+
const [shabbatRes, holidayRes] = await Promise.all([fetch(shabbatUrl), fetch(holidayUrl)]);
|
|
48
|
+
if (!shabbatRes.ok || !holidayRes.ok) {
|
|
49
|
+
throw new Error(`hebcal fetch failed: shabbat=${shabbatRes.status} holiday=${holidayRes.status}`);
|
|
50
|
+
}
|
|
51
|
+
const [shabbatData, holidayData] = (await Promise.all([
|
|
52
|
+
shabbatRes.json(),
|
|
53
|
+
holidayRes.json(),
|
|
54
|
+
]));
|
|
55
|
+
const windows = [
|
|
56
|
+
...pairWindows(shabbatData.items ?? []),
|
|
57
|
+
...pairWindows(holidayData.items ?? []),
|
|
58
|
+
];
|
|
59
|
+
return windows.sort((a, b) => a.start - b.start);
|
|
60
|
+
}
|
|
61
|
+
/** Pure function: is `now` inside any of the given windows? */
|
|
62
|
+
export function isBlocked(windows, now) {
|
|
63
|
+
return findActiveWindow(windows, now) !== undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Pure function: the window covering `now`, if any. */
|
|
66
|
+
export function findActiveWindow(windows, now) {
|
|
67
|
+
return windows.find((w) => w.start <= now && now < w.end);
|
|
68
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface HoldingPageContext {
|
|
2
|
+
siteName: string;
|
|
3
|
+
reasonLabel: string;
|
|
4
|
+
untilLabel: string;
|
|
5
|
+
}
|
|
6
|
+
/** Simple, centered, mobile-responsive holding page. Inline CSS only, no external assets. */
|
|
7
|
+
export declare function defaultRenderHoldingPage(ctx: HoldingPageContext): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Simple, centered, mobile-responsive holding page. Inline CSS only, no external assets. */
|
|
2
|
+
export function defaultRenderHoldingPage(ctx) {
|
|
3
|
+
return `<!doctype html>
|
|
4
|
+
<html lang="he" dir="rtl">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<meta name="robots" content="noindex">
|
|
9
|
+
<title>${ctx.siteName}</title>
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; }
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
19
|
+
background: #0f2138;
|
|
20
|
+
color: #f3efe4;
|
|
21
|
+
text-align: center;
|
|
22
|
+
padding: 24px;
|
|
23
|
+
}
|
|
24
|
+
.card { max-width: 480px; }
|
|
25
|
+
h1 { font-size: 1.5rem; margin: 0 0 12px; }
|
|
26
|
+
p { font-size: 1rem; line-height: 1.6; color: #c8a951; margin: 0 0 8px; }
|
|
27
|
+
.until { font-size: 0.9rem; color: #9fb0c4; }
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div class="card">
|
|
32
|
+
<h1>${ctx.siteName}</h1>
|
|
33
|
+
<p>האתר סגור לכבוד ${ctx.reasonLabel}, ניפגש שוב אחרי הצאת ${ctx.reasonLabel}.</p>
|
|
34
|
+
<p class="until">שעת פתיחה משוערת: ${ctx.untilLabel}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</body>
|
|
37
|
+
</html>`;
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type HoldingPageContext } from './holdingPage.js';
|
|
2
|
+
export type { Window } from './hebcal.js';
|
|
3
|
+
export type { HoldingPageContext } from './holdingPage.js';
|
|
4
|
+
export { isBlocked, findActiveWindow, pairWindows, fetchWindows } from './hebcal.js';
|
|
5
|
+
export { isBot, BOT_PATTERN } from './botPattern.js';
|
|
6
|
+
export { defaultRenderHoldingPage } from './holdingPage.js';
|
|
7
|
+
export interface ShabbatGateConfig {
|
|
8
|
+
siteName: string;
|
|
9
|
+
/** Decimal lat/long for zmanim. Both default to Jerusalem if omitted - a fine
|
|
10
|
+
* single reference point for all of Israel at this granularity. */
|
|
11
|
+
latitude?: number;
|
|
12
|
+
longitude?: number;
|
|
13
|
+
/** Query param name + required value that bypasses the gate entirely, for
|
|
14
|
+
* the site owner to preview/test on any day. Keep the value non-guessable -
|
|
15
|
+
* this is a testing convenience, not real auth. */
|
|
16
|
+
bypassParam?: string;
|
|
17
|
+
bypassValue?: string;
|
|
18
|
+
/** Optional custom holding-page renderer. Defaults to a Hebrew, mobile-
|
|
19
|
+
* responsive page showing siteName + when the site reopens. */
|
|
20
|
+
renderHoldingPage?: (ctx: HoldingPageContext) => string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns a Cloudflare Pages Functions-compatible handler that closes the
|
|
24
|
+
* site to human visitors during Shabbat and major Jewish holidays, while
|
|
25
|
+
* always letting search engines and AI crawlers through. Fails open on any
|
|
26
|
+
* error - an accidental block on a regular Tuesday is a real, visible bug; an
|
|
27
|
+
* occasional missed block during an error is a minor, invisible one.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createShabbatGate(config: ShabbatGateConfig): PagesFunction;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isBot } from './botPattern.js';
|
|
2
|
+
import { fetchWindows, findActiveWindow } from './hebcal.js';
|
|
3
|
+
import { defaultRenderHoldingPage } from './holdingPage.js';
|
|
4
|
+
export { isBlocked, findActiveWindow, pairWindows, fetchWindows } from './hebcal.js';
|
|
5
|
+
export { isBot, BOT_PATTERN } from './botPattern.js';
|
|
6
|
+
export { defaultRenderHoldingPage } from './holdingPage.js';
|
|
7
|
+
const JERUSALEM_LATITUDE = 31.7683;
|
|
8
|
+
const JERUSALEM_LONGITUDE = 35.2137;
|
|
9
|
+
const CACHE_KEY_URL = 'https://internal.cache/shabbat-gate-windows-v1';
|
|
10
|
+
const CACHE_TTL_SECONDS = 24 * 60 * 60;
|
|
11
|
+
async function getWindows(latitude, longitude) {
|
|
12
|
+
const cache = caches.default;
|
|
13
|
+
const cacheRequest = new Request(CACHE_KEY_URL);
|
|
14
|
+
const cached = await cache.match(cacheRequest);
|
|
15
|
+
if (cached) {
|
|
16
|
+
return (await cached.json());
|
|
17
|
+
}
|
|
18
|
+
const windows = await fetchWindows(latitude, longitude);
|
|
19
|
+
const cacheResponse = new Response(JSON.stringify(windows), {
|
|
20
|
+
headers: {
|
|
21
|
+
'content-type': 'application/json',
|
|
22
|
+
'cache-control': `max-age=${CACHE_TTL_SECONDS}`,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
await cache.put(cacheRequest, cacheResponse);
|
|
26
|
+
return windows;
|
|
27
|
+
}
|
|
28
|
+
function formatJerusalemTime(epochMs) {
|
|
29
|
+
return new Intl.DateTimeFormat('he-IL', {
|
|
30
|
+
timeZone: 'Asia/Jerusalem',
|
|
31
|
+
day: '2-digit',
|
|
32
|
+
month: '2-digit',
|
|
33
|
+
hour: '2-digit',
|
|
34
|
+
minute: '2-digit',
|
|
35
|
+
}).format(new Date(epochMs));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns a Cloudflare Pages Functions-compatible handler that closes the
|
|
39
|
+
* site to human visitors during Shabbat and major Jewish holidays, while
|
|
40
|
+
* always letting search engines and AI crawlers through. Fails open on any
|
|
41
|
+
* error - an accidental block on a regular Tuesday is a real, visible bug; an
|
|
42
|
+
* occasional missed block during an error is a minor, invisible one.
|
|
43
|
+
*/
|
|
44
|
+
export function createShabbatGate(config) {
|
|
45
|
+
return async (context) => {
|
|
46
|
+
const { request, next } = context;
|
|
47
|
+
const userAgent = request.headers.get('user-agent') ?? '';
|
|
48
|
+
if (isBot(userAgent)) {
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
if (config.bypassParam && config.bypassValue) {
|
|
52
|
+
const url = new URL(request.url);
|
|
53
|
+
if (url.searchParams.get(config.bypassParam) === config.bypassValue) {
|
|
54
|
+
return next();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const latitude = config.latitude ?? JERUSALEM_LATITUDE;
|
|
59
|
+
const longitude = config.longitude ?? JERUSALEM_LONGITUDE;
|
|
60
|
+
const windows = await getWindows(latitude, longitude);
|
|
61
|
+
const active = findActiveWindow(windows, Date.now());
|
|
62
|
+
if (!active) {
|
|
63
|
+
return next();
|
|
64
|
+
}
|
|
65
|
+
const render = config.renderHoldingPage ?? defaultRenderHoldingPage;
|
|
66
|
+
const html = render({
|
|
67
|
+
siteName: config.siteName,
|
|
68
|
+
reasonLabel: active.label,
|
|
69
|
+
untilLabel: formatJerusalemTime(active.end),
|
|
70
|
+
});
|
|
71
|
+
return new Response(html, {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error('shabbat-gate: failing open due to error', error);
|
|
78
|
+
return next();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shabbat-gate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cloudflare Pages/Workers middleware that closes a site to human visitors during Shabbat and major Jewish holidays (Israel-observance rules), while always letting search engines and AI crawlers through.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "vitest run"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cloudflare-pages",
|
|
19
|
+
"cloudflare-workers",
|
|
20
|
+
"shabbat",
|
|
21
|
+
"jewish-holidays",
|
|
22
|
+
"hebcal",
|
|
23
|
+
"israel"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@cloudflare/workers-types": "^4.20250204.0",
|
|
31
|
+
"typescript": "^5.7.3",
|
|
32
|
+
"vitest": "^3.0.5"
|
|
33
|
+
}
|
|
34
|
+
}
|