@stnd/modules 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Backlinks.astro +56 -0
- package/CHANGELOG.md +60 -0
- package/Comments.astro +115 -0
- package/README.md +404 -0
- package/ReadingTime.astro +60 -0
- package/brand-manual/index.module.js +12 -0
- package/brand-manual/route.astro +341 -0
- package/confetti/Confetti.astro +42 -0
- package/confetti/index.module.js +22 -0
- package/content/index.module.js +19 -0
- package/content/route.astro +59 -0
- package/copy-buttons/index.module.js +9 -0
- package/deep-link/index.module.js +9 -0
- package/deep-link/init.js +14 -0
- package/eink/index.module.js +9 -0
- package/fonts/index.module.js +76 -0
- package/gestures/index.module.js +9 -0
- package/gestures/init.js +13 -0
- package/gsap/client.js +41 -0
- package/gsap/index.module.js +20 -0
- package/headers/index.module.js +16 -0
- package/headers/route.js +93 -0
- package/humans/index.module.js +17 -0
- package/humans/route.js +79 -0
- package/iconify/index.module.js +9 -0
- package/image-zoom/index.module.js +9 -0
- package/keyboard/index.module.js +9 -0
- package/lab/index.module.js +26 -0
- package/launcher/index.module.js +27 -0
- package/maintenance/index.module.js +18 -0
- package/maintenance/middleware.js +34 -0
- package/maintenance/routes/index.astro +45 -0
- package/manifest/index.module.js +16 -0
- package/manifest/route.js +57 -0
- package/math/client.js +98 -0
- package/math/index.module.js +19 -0
- package/math/styles.css +25 -0
- package/mermaid/client.js +125 -0
- package/mermaid/index.module.js +19 -0
- package/mermaid/styles.css +114 -0
- package/p5/client.js +25 -0
- package/p5/index.module.js +24 -0
- package/package.json +35 -0
- package/prism/README.md +13 -0
- package/prism/index.module.js +19 -0
- package/robots/index.module.js +16 -0
- package/robots/route.js +67 -0
- package/rss/index.module.js +18 -0
- package/rss/route.js +163 -0
- package/scroll-wrappers/index.module.js +9 -0
- package/security-txt/index.module.js +17 -0
- package/security-txt/route.js +90 -0
- package/sitemap/index.module.js +22 -0
- package/stripe/client.js +27 -0
- package/stripe/components/MockBuyButton.svelte +38 -0
- package/stripe/index.module.js +33 -0
- package/stripe/routes/mock-checkout.js +64 -0
- package/stripe/views/CheckoutView.svelte +181 -0
- package/styles/index.module.js +11 -0
- package/theme-utils/README.md +74 -0
- package/theme-utils/client.js +79 -0
- package/theme-utils/index.module.js +27 -0
- package/themes/index.module.js +50 -0
- package/toast/index.module.js +17 -0
- package/toast/toast.js +17 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid Module — Client Initializer
|
|
3
|
+
*
|
|
4
|
+
* Standalone module that detects and renders Mermaid diagrams.
|
|
5
|
+
* Decoupled from the markdown renderer.
|
|
6
|
+
*
|
|
7
|
+
* Theme-aware: Uses native CSS variables via styles.css for instant color transitions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MERMAID_CDN =
|
|
11
|
+
"https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
|
12
|
+
|
|
13
|
+
let mermaidLib = null;
|
|
14
|
+
|
|
15
|
+
async function loadMermaid() {
|
|
16
|
+
if (mermaidLib) return mermaidLib;
|
|
17
|
+
if (window.mermaid) {
|
|
18
|
+
mermaidLib = window.mermaid;
|
|
19
|
+
return mermaidLib;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const mod = await import(/* @vite-ignore */ MERMAID_CDN);
|
|
23
|
+
mermaidLib = mod.default;
|
|
24
|
+
window.mermaid = mermaidLib;
|
|
25
|
+
return mermaidLib;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error("[Mermaid] Failed to load from CDN:", err);
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveFont(name, fallback) {
|
|
33
|
+
if (typeof window === "undefined") return fallback;
|
|
34
|
+
const raw = getComputedStyle(document.documentElement)
|
|
35
|
+
.getPropertyValue(name)
|
|
36
|
+
.trim();
|
|
37
|
+
return raw || fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Clean up characters corrupted by typography post-processing or editors.
|
|
42
|
+
*/
|
|
43
|
+
function cleanMermaidCode(raw) {
|
|
44
|
+
return raw
|
|
45
|
+
.trim()
|
|
46
|
+
.replace(/[\u201C\u201D]/g, '"')
|
|
47
|
+
.replace(/[\u2018\u2019]/g, "'")
|
|
48
|
+
.replace(/\u2192/g, "->")
|
|
49
|
+
.replace(/\u2190/g, "<-")
|
|
50
|
+
.replace(/\u2014>/g, "-->")
|
|
51
|
+
.replace(/\u2014/g, "--")
|
|
52
|
+
.replace(/\u2013/g, "-")
|
|
53
|
+
.replace(/\u2026/g, "...")
|
|
54
|
+
.replace(/->/g, "->")
|
|
55
|
+
.replace(/<-/g, "<-")
|
|
56
|
+
.replace(/&/g, "&")
|
|
57
|
+
.replace(/"/g, '"')
|
|
58
|
+
.replace(/'/g, "'");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Render a single diagram into a container.
|
|
63
|
+
* Returns the container element (or null on failure).
|
|
64
|
+
*/
|
|
65
|
+
async function renderOne(lib, code, index) {
|
|
66
|
+
try {
|
|
67
|
+
const { svg } = await lib.render(`mmd-${Date.now()}-${index}`, code);
|
|
68
|
+
const container = document.createElement("div");
|
|
69
|
+
container.className = "mermaid-rendered";
|
|
70
|
+
container.dataset.mermaidSource = code;
|
|
71
|
+
container.innerHTML = svg;
|
|
72
|
+
return container;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error("[Mermaid] Render error:", err);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* First-pass render: find unprocessed code blocks and render them.
|
|
81
|
+
*/
|
|
82
|
+
async function renderMermaid() {
|
|
83
|
+
const blocks = document.querySelectorAll(
|
|
84
|
+
".mermaid:not([data-mermaid-processed]), pre code.language-mermaid:not([data-mermaid-processed])",
|
|
85
|
+
);
|
|
86
|
+
if (blocks.length === 0) return;
|
|
87
|
+
|
|
88
|
+
let lib;
|
|
89
|
+
try {
|
|
90
|
+
lib = await loadMermaid();
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lib.initialize({
|
|
96
|
+
startOnLoad: false,
|
|
97
|
+
theme: "base",
|
|
98
|
+
securityLevel: "loose",
|
|
99
|
+
fontFamily: resolveFont("--font-monospace", "sans-serif"),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
103
|
+
const block = blocks[i];
|
|
104
|
+
block.setAttribute("data-mermaid-processed", "true");
|
|
105
|
+
|
|
106
|
+
const code = cleanMermaidCode(block.textContent || "");
|
|
107
|
+
if (!code) continue;
|
|
108
|
+
|
|
109
|
+
const container = await renderOne(lib, code, i);
|
|
110
|
+
if (container) {
|
|
111
|
+
const pre = block.closest("pre");
|
|
112
|
+
(pre || block).replaceWith(container);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Exports ──────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function init() {
|
|
120
|
+
renderMermaid();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function refresh() {
|
|
124
|
+
renderMermaid();
|
|
125
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid Module — Standalone diagram and flowchart rendering.
|
|
3
|
+
*
|
|
4
|
+
* Scans the page for Mermaid syntax and renders it via Mermaid.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
id: "mermaid",
|
|
9
|
+
name: "Mermaid",
|
|
10
|
+
description: "Diagram and flowchart rendering with Mermaid.js",
|
|
11
|
+
|
|
12
|
+
styles: ["./styles.css"],
|
|
13
|
+
scripts: [{ src: "./client.js", init: "init", refresh: "refresh" }],
|
|
14
|
+
|
|
15
|
+
metadata: {
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
tags: ["diagram", "mermaid", "flowchart"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* Mermaid Module Styles
|
|
2
|
+
*
|
|
3
|
+
* Mermaid renders SVGs with inline styles using themeVariables from client.js.
|
|
4
|
+
* These CSS rules handle layout and container cleanup, and override colors
|
|
5
|
+
* with CSS custom properties so that theme transitions are instant and native.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.stnd-code-block:has(.mermaid-rendered),
|
|
9
|
+
.mermaid-rendered {
|
|
10
|
+
text-box-trim: none !important;
|
|
11
|
+
background: transparent;
|
|
12
|
+
box-shadow: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.mermaid-rendered {
|
|
16
|
+
text-box-trim: none !important;
|
|
17
|
+
text-align: center;
|
|
18
|
+
|
|
19
|
+
foreignObject {
|
|
20
|
+
overflow: visible;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ── Instant CSS Variable Overrides ─────────────────────────────────────── */
|
|
25
|
+
/* Node fills and borders */
|
|
26
|
+
.mermaid-rendered svg .node rect,
|
|
27
|
+
.mermaid-rendered svg .node circle,
|
|
28
|
+
.mermaid-rendered svg .node ellipse,
|
|
29
|
+
.mermaid-rendered svg .node polygon,
|
|
30
|
+
.mermaid-rendered svg .node path {
|
|
31
|
+
fill: var(--color-surface) !important;
|
|
32
|
+
stroke: var(--color-border) !important;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Cluster/subgraph backgrounds */
|
|
36
|
+
.mermaid-rendered svg .cluster rect {
|
|
37
|
+
fill: var(--color-background) !important;
|
|
38
|
+
stroke: var(--color-border) !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Edges and lines */
|
|
42
|
+
.mermaid-rendered svg .edgePath .path,
|
|
43
|
+
.mermaid-rendered svg .flowchart-link {
|
|
44
|
+
stroke: var(--color-border) !important;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Arrowheads/markers */
|
|
48
|
+
.mermaid-rendered svg marker path {
|
|
49
|
+
fill: var(--color-border) !important;
|
|
50
|
+
stroke: var(--color-border) !important;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Text / labels */
|
|
54
|
+
.mermaid-rendered svg .label text,
|
|
55
|
+
.mermaid-rendered svg .nodeLabel,
|
|
56
|
+
.mermaid-rendered svg .edgeLabel,
|
|
57
|
+
.mermaid-rendered svg .cluster-label text,
|
|
58
|
+
.mermaid-rendered svg .edgeLabel p,
|
|
59
|
+
.mermaid-rendered svg .edgeLabel span,
|
|
60
|
+
.mermaid-rendered svg .nodeLabel p,
|
|
61
|
+
.mermaid-rendered svg .nodeLabel span {
|
|
62
|
+
fill: var(--color-foreground) !important;
|
|
63
|
+
color: var(--color-foreground) !important;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.mermaid-rendered svg .edgeLabel,
|
|
67
|
+
.mermaid-rendered svg .edgeLabel div,
|
|
68
|
+
.mermaid-rendered svg .edgeLabel span,
|
|
69
|
+
.mermaid-rendered svg .edgeLabel p,
|
|
70
|
+
.mermaid-rendered svg .edgeTerminals {
|
|
71
|
+
background-color: var(--color-background) !important;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.mermaid-rendered svg .edgeLabel rect {
|
|
75
|
+
fill: var(--color-background) !important;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Sequence diagrams */
|
|
79
|
+
.mermaid-rendered svg .actor {
|
|
80
|
+
fill: var(--color-surface) !important;
|
|
81
|
+
stroke: var(--color-border) !important;
|
|
82
|
+
}
|
|
83
|
+
.mermaid-rendered svg text.actor > tspan {
|
|
84
|
+
fill: var(--color-foreground) !important;
|
|
85
|
+
}
|
|
86
|
+
.mermaid-rendered svg .actor-line {
|
|
87
|
+
stroke: var(--color-border) !important;
|
|
88
|
+
}
|
|
89
|
+
.mermaid-rendered svg .messageLine0,
|
|
90
|
+
.mermaid-rendered svg .messageLine1 {
|
|
91
|
+
stroke: var(--color-border) !important;
|
|
92
|
+
}
|
|
93
|
+
.mermaid-rendered svg #arrowhead path,
|
|
94
|
+
.mermaid-rendered svg #crosshead path,
|
|
95
|
+
.mermaid-rendered svg #filled-head path {
|
|
96
|
+
fill: var(--color-border) !important;
|
|
97
|
+
stroke: none !important;
|
|
98
|
+
}
|
|
99
|
+
.mermaid-rendered svg .messageText {
|
|
100
|
+
fill: var(--color-foreground) !important;
|
|
101
|
+
stroke: none !important;
|
|
102
|
+
}
|
|
103
|
+
.mermaid-rendered svg .note {
|
|
104
|
+
fill: var(--color-surface) !important;
|
|
105
|
+
stroke: var(--color-border) !important;
|
|
106
|
+
}
|
|
107
|
+
.mermaid-rendered svg .noteText > tspan {
|
|
108
|
+
fill: var(--color-foreground) !important;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Default SVG text fallback */
|
|
112
|
+
.mermaid-rendered svg text {
|
|
113
|
+
fill: var(--color-foreground) !important;
|
|
114
|
+
}
|
package/p5/client.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* p5 Module — Client accessor
|
|
3
|
+
*
|
|
4
|
+
* The p5.js UMD build is preloaded in <head> by the module manifest's
|
|
5
|
+
* scripts: field, so window.p5 is available by the time any Svelte island
|
|
6
|
+
* mounts. No async fetch needed.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { getP5 } from "@stnd/modules/p5/client.js";
|
|
10
|
+
*
|
|
11
|
+
* onMount(() => {
|
|
12
|
+
* const p5 = getP5();
|
|
13
|
+
* instance = new p5(sketch, container);
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the p5 constructor from window.
|
|
19
|
+
* Must be called inside onMount (browser only).
|
|
20
|
+
* @returns {typeof p5}
|
|
21
|
+
*/
|
|
22
|
+
export function getP5() {
|
|
23
|
+
if (!window.p5) throw new Error("[p5] window.p5 not found — is the p5 module loaded?");
|
|
24
|
+
return window.p5;
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* p5 Module — Creative coding with p5.js
|
|
3
|
+
*
|
|
4
|
+
* Provides a CDN-backed lazy loader for p5.js.
|
|
5
|
+
* No bundling — p5 is loaded at runtime from jsDelivr.
|
|
6
|
+
*
|
|
7
|
+
* Usage in components:
|
|
8
|
+
* import { loadP5 } from "@stnd/modules/p5/client.js";
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: "p5",
|
|
13
|
+
name: "p5",
|
|
14
|
+
description: "Creative coding with p5.js — preloaded from CDN, available as window.p5",
|
|
15
|
+
|
|
16
|
+
// UMD build: loads in <head> in parallel with the page, sets window.p5.
|
|
17
|
+
// By the time a Svelte island mounts, window.p5 is already available.
|
|
18
|
+
scripts: ["https://cdn.jsdelivr.net/npm/p5@1/lib/p5.min.js"],
|
|
19
|
+
|
|
20
|
+
metadata: {
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
tags: ["canvas", "creative-coding", "p5", "generative-art"],
|
|
23
|
+
},
|
|
24
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stnd/modules",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"svelte": "./stripe/views/CheckoutView.svelte",
|
|
6
|
+
"author": "Francis Fontaine",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./toast/index.module.js",
|
|
10
|
+
"./*": {
|
|
11
|
+
"astro": "./*.astro",
|
|
12
|
+
"svelte": "./*.svelte",
|
|
13
|
+
"js": "./*.js",
|
|
14
|
+
"default": "./*/index.module.js"
|
|
15
|
+
},
|
|
16
|
+
"./*/*": "./*/*"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@astrojs/sitemap": "^3.7.3",
|
|
20
|
+
"wrangler": "^4.105.0",
|
|
21
|
+
"@stnd/icon": "0.2.0",
|
|
22
|
+
"@stnd/log": "0.5.0",
|
|
23
|
+
"@stnd/styles": "0.5.2",
|
|
24
|
+
"@stnd/utils": "0.5.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"astro": ">=5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@better-auth/cli": "^1.4.21"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/prism/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Prism Syntax Highlighting Module
|
|
2
|
+
|
|
3
|
+
This module loads Prism.js and Prism.css from CDN and ensures syntax highlighting is applied to all code blocks on initial load and after every Astro navigation ("astro:after-swap").
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
- Register this module in your app's module manifest.
|
|
8
|
+
- All code blocks with appropriate language classes will be highlighted automatically.
|
|
9
|
+
|
|
10
|
+
## CDN Versions
|
|
11
|
+
|
|
12
|
+
- Prism.js v1.29.0
|
|
13
|
+
- Prism.css v1.29.0
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism Syntax Highlighting Module
|
|
3
|
+
* Loads Prism.js and Prism.css from CDN and highlights code blocks on every page change.
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
* ,
|
|
7
|
+
styles: [
|
|
8
|
+
"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css",
|
|
9
|
+
],
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
id: "stnd-prism",
|
|
13
|
+
name: "Stnd :: Prism Syntax Highlighting",
|
|
14
|
+
scripts: [
|
|
15
|
+
"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js",
|
|
16
|
+
"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js",
|
|
17
|
+
{ src: "@stnd/client/prism", init: "initPrism", refresh: "refreshPrism" },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Robots.txt core module
|
|
3
|
+
*
|
|
4
|
+
* Exposes the /robots.txt route via the Standard module system.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
id: "stnd-robots",
|
|
8
|
+
name: "Stnd :: Robots",
|
|
9
|
+
routes: [
|
|
10
|
+
{
|
|
11
|
+
path: "/robots.txt",
|
|
12
|
+
entrypoint: "./route.js",
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
description: "Serves robots.txt based on site configuration.",
|
|
16
|
+
};
|
package/robots/route.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
|
|
3
|
+
export async function GET(context) {
|
|
4
|
+
const robots = config.robots || {};
|
|
5
|
+
if (robots.enabled === false) {
|
|
6
|
+
return new Response(null, { status: 404 });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const site = context.site ? context.site.toString() : "https://example.com";
|
|
10
|
+
const userAgent = robots.userAgent || "*";
|
|
11
|
+
const disallow = robots.disallow || ["/admin/", "/private/"];
|
|
12
|
+
const allow = robots.allow || [];
|
|
13
|
+
const customRules = robots.customRules || [];
|
|
14
|
+
const crawlDelay = robots.crawlDelay;
|
|
15
|
+
const sitemapUrl =
|
|
16
|
+
robots.sitemapUrl ||
|
|
17
|
+
(context.site ? new URL("sitemap.xml", context.site).href : null);
|
|
18
|
+
|
|
19
|
+
let content = `# robots.txt for ${site}
|
|
20
|
+
# Generated by Standard Framework
|
|
21
|
+
|
|
22
|
+
User-agent: ${userAgent}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
if (disallow.length > 0) {
|
|
26
|
+
disallow.forEach((p) => {
|
|
27
|
+
content += `Disallow: ${p}
|
|
28
|
+
`;
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
content += `Disallow:
|
|
32
|
+
`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
allow.forEach((p) => {
|
|
36
|
+
content += `Allow: ${p}
|
|
37
|
+
`;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (crawlDelay) {
|
|
41
|
+
content += `Crawl-delay: ${crawlDelay}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (customRules.length > 0) {
|
|
46
|
+
content += `
|
|
47
|
+
# Custom rules
|
|
48
|
+
`;
|
|
49
|
+
customRules.forEach((rule) => {
|
|
50
|
+
content += `${rule}
|
|
51
|
+
`;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (sitemapUrl) {
|
|
56
|
+
content += `
|
|
57
|
+
# Sitemap
|
|
58
|
+
Sitemap: ${sitemapUrl}
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response(content, {
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "text/plain",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSS/Atom Feed core module
|
|
3
|
+
*
|
|
4
|
+
* Exposes the /rss.xml route via the Standard module system.
|
|
5
|
+
* Auto-generates a feed from content collections and site config.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
id: "stnd-rss",
|
|
9
|
+
name: "Stnd :: RSS",
|
|
10
|
+
description:
|
|
11
|
+
"Generates an RSS/Atom feed from site content and configuration.",
|
|
12
|
+
routes: [
|
|
13
|
+
{
|
|
14
|
+
path: "/rss.xml",
|
|
15
|
+
entrypoint: "./route.js",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
package/rss/route.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import config from "virtual:stnd/config";
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { runHook } from "virtual:stnd/hooks";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RSS 2.0 Feed Route
|
|
7
|
+
*
|
|
8
|
+
* Generates a standards-compliant RSS feed.
|
|
9
|
+
* Aggregates items from:
|
|
10
|
+
* 1. Static config (config.rss.items)
|
|
11
|
+
* 2. Dynamic hook (stnd:rss:register)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function escapeXml(str) {
|
|
15
|
+
if (!str) return "";
|
|
16
|
+
return String(str)
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, """)
|
|
21
|
+
.replace(/'/g, "'");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatRfc822Date(input) {
|
|
25
|
+
if (!input) return null;
|
|
26
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
27
|
+
if (isNaN(date.getTime())) return null;
|
|
28
|
+
return date.toUTCString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function GET(context) {
|
|
32
|
+
const rss = config.rss || {};
|
|
33
|
+
if (rss.enabled === false) {
|
|
34
|
+
return new Response(null, { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const site = context.site
|
|
38
|
+
? context.site.toString().replace(/\/$/, "")
|
|
39
|
+
: config.url?.replace(/\/$/, "") || "https://example.com";
|
|
40
|
+
|
|
41
|
+
// 1. Collect Items
|
|
42
|
+
// --- Static items from config
|
|
43
|
+
const staticItems = rss.items || [];
|
|
44
|
+
|
|
45
|
+
// --- Dynamic items via Hook
|
|
46
|
+
let dynamicItems = [];
|
|
47
|
+
try {
|
|
48
|
+
const hookResults = await runHook("stnd:rss:register");
|
|
49
|
+
dynamicItems = hookResults.flat().filter(Boolean);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error("[RSS] Hook stnd:rss:register failed:", e);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 2. Merge, Sort and Limit
|
|
55
|
+
const allItems = [...staticItems, ...dynamicItems]
|
|
56
|
+
.map((item) => ({
|
|
57
|
+
...item,
|
|
58
|
+
parsedDate: item.pubDate ? new Date(item.pubDate) : new Date(0),
|
|
59
|
+
}))
|
|
60
|
+
.sort((a, b) => b.parsedDate.getTime() - a.parsedDate.getTime())
|
|
61
|
+
.slice(0, rss.limit || 50);
|
|
62
|
+
|
|
63
|
+
const title = rss.title || config.title || "Untitled Site";
|
|
64
|
+
const description = rss.description || config.description || "";
|
|
65
|
+
const language = rss.language || config.language || "fr";
|
|
66
|
+
const copyright = rss.copyright || "";
|
|
67
|
+
const managingEditor = rss.managingEditor || "";
|
|
68
|
+
const webMaster = rss.webMaster || "";
|
|
69
|
+
const ttl = rss.ttl || 60;
|
|
70
|
+
const categories = rss.categories || [];
|
|
71
|
+
|
|
72
|
+
const feedUrl = new URL("/rss.xml", site).href;
|
|
73
|
+
const lastBuildDate = formatRfc822Date(new Date());
|
|
74
|
+
|
|
75
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
76
|
+
<rss version="2.0"
|
|
77
|
+
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
78
|
+
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
|
79
|
+
xmlns:media="http://search.yahoo.com/mrss/"
|
|
80
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
81
|
+
<channel>
|
|
82
|
+
<title>${escapeXml(title)}</title>
|
|
83
|
+
<link>${escapeXml(site)}</link>
|
|
84
|
+
<description>${escapeXml(description)}</description>
|
|
85
|
+
<language>${escapeXml(language)}</language>
|
|
86
|
+
<lastBuildDate>${lastBuildDate}</lastBuildDate>
|
|
87
|
+
<atom:link href="${escapeXml(feedUrl)}" rel="self" type="application/rss+xml" />
|
|
88
|
+
<generator>Standard Framework</generator>
|
|
89
|
+
<ttl>${ttl}</ttl>
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
if (copyright) {
|
|
93
|
+
xml += ` <copyright>${escapeXml(copyright)}</copyright>\n`;
|
|
94
|
+
}
|
|
95
|
+
if (managingEditor) {
|
|
96
|
+
xml += ` <managingEditor>${escapeXml(managingEditor)}</managingEditor>\n`;
|
|
97
|
+
}
|
|
98
|
+
if (webMaster) {
|
|
99
|
+
xml += ` <webMaster>${escapeXml(webMaster)}</webMaster>\n`;
|
|
100
|
+
}
|
|
101
|
+
for (const cat of categories) {
|
|
102
|
+
xml += ` <category>${escapeXml(cat)}</category>\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const item of allItems) {
|
|
106
|
+
const itemLink = item.link
|
|
107
|
+
? item.link.startsWith("http")
|
|
108
|
+
? item.link
|
|
109
|
+
: new URL(item.link, site).href
|
|
110
|
+
: "";
|
|
111
|
+
const itemGuid = item.guid
|
|
112
|
+
? item.guid.startsWith("http")
|
|
113
|
+
? item.guid
|
|
114
|
+
: new URL(item.guid, site).href
|
|
115
|
+
: itemLink;
|
|
116
|
+
const pubDate = formatRfc822Date(item.pubDate);
|
|
117
|
+
const itemCategories = item.categories || item.tags || [];
|
|
118
|
+
|
|
119
|
+
xml += ` <item>
|
|
120
|
+
<title>${escapeXml(item.title || "Untitled")}</title>
|
|
121
|
+
<link>${escapeXml(itemLink)}</link>
|
|
122
|
+
<guid isPermaLink="true">${escapeXml(itemGuid)}</guid>
|
|
123
|
+
`;
|
|
124
|
+
if (item.description) {
|
|
125
|
+
xml += ` <description>${escapeXml(item.description)}</description>\n`;
|
|
126
|
+
}
|
|
127
|
+
if (item.content) {
|
|
128
|
+
xml += ` <content:encoded><![CDATA[${item.content}]]></content:encoded>\n`;
|
|
129
|
+
}
|
|
130
|
+
if (pubDate) {
|
|
131
|
+
xml += ` <pubDate>${pubDate}</pubDate>\n`;
|
|
132
|
+
}
|
|
133
|
+
if (item.author) {
|
|
134
|
+
xml += ` <dc:creator>${escapeXml(item.author)}</dc:creator>\n`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Support for Images (Media RSS & Enclosure)
|
|
138
|
+
if (item.image) {
|
|
139
|
+
const imgUrl = item.image.startsWith("http")
|
|
140
|
+
? item.image
|
|
141
|
+
: new URL(item.image, site).href;
|
|
142
|
+
xml += ` <enclosure url="${escapeXml(imgUrl)}" length="0" type="image/jpeg" />\n`;
|
|
143
|
+
xml += ` <media:content url="${escapeXml(imgUrl)}" medium="image" />\n`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const cat of itemCategories) {
|
|
147
|
+
xml += ` <category>${escapeXml(cat)}</category>\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
xml += ` </item>\n`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
xml += ` </channel>
|
|
154
|
+
</rss>
|
|
155
|
+
`;
|
|
156
|
+
|
|
157
|
+
return new Response(xml, {
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "application/rss+xml; charset=utf-8",
|
|
160
|
+
"Cache-Control": "public, max-age=3600, stale-while-revalidate=86400",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scroll Wrappers Module
|
|
3
|
+
* Client script lives in @stnd/client — manifest registered here.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
id: "stnd-scroll-wrappers",
|
|
7
|
+
name: "Stnd :: Client :: Scroll Wrappers",
|
|
8
|
+
scripts: [{ src: "@stnd/client/scroll-wrappers", init: "initScrollWrappers", refresh: "refreshScrollWrappers" }],
|
|
9
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* security.txt core module
|
|
3
|
+
*
|
|
4
|
+
* Serves /.well-known/security.txt per RFC 9116.
|
|
5
|
+
* Configurable via standard({ security: { securityTxt: { ... } } }).
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
id: "stnd-security-txt",
|
|
9
|
+
name: "Stnd :: security.txt",
|
|
10
|
+
description: "Serves /.well-known/security.txt per RFC 9116.",
|
|
11
|
+
routes: [
|
|
12
|
+
{
|
|
13
|
+
path: "/.well-known/security.txt",
|
|
14
|
+
entrypoint: "./route.js",
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|