@profullstack/footer 0.2.6 → 0.3.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/README.md +13 -0
- package/bin/profullstack-footer.js +3 -0
- package/index.d.ts +4 -0
- package/package.json +1 -1
- package/src/index.js +2 -0
- package/src/render.js +25 -0
- package/src/template.js +13 -1
- package/template.json +3 -3
package/README.md
CHANGED
|
@@ -72,11 +72,24 @@ npx @profullstack/footer html --site https://example.com/ --link Docs=/docs
|
|
|
72
72
|
| `ring` | `{ slug: 'profullstack', name: 'Profullstack', host: 'https://rssamplifier.com' }` | |
|
|
73
73
|
| `css` | `true` | `false` drops the `<style>`; style `.pfs-footer*` yourself |
|
|
74
74
|
| `houseLinks` | `true` | `false` hides links the template adds for every site |
|
|
75
|
+
| `subscribe` | `true` | `false` hides the newsletter form |
|
|
75
76
|
| `nonce` | | CSP nonce for the inline `<style>`; or `css: false` and style `.pfs-footer*` yourself |
|
|
76
77
|
|
|
77
78
|
Theme it with `--pfs-footer-muted`, `--pfs-footer-gap` and `--pfs-footer-size` on
|
|
78
79
|
`.pfs-footer`; colors otherwise inherit from the page.
|
|
79
80
|
|
|
81
|
+
## Newsletter form
|
|
82
|
+
|
|
83
|
+
Every footer carries a small email field and a Subscribe button: a plain HTML form (no script)
|
|
84
|
+
that posts to `https://pit.world/subscribe` with the site's hostname. pit.world runs the double
|
|
85
|
+
opt-in (confirmation email, nothing kept until the click), answers with a "check your inbox" page
|
|
86
|
+
that links back to the site, and records which property each subscriber came from
|
|
87
|
+
(`https://pit.world/admin/subscribers`). The endpoint is fixed in render.js, never in the template.
|
|
88
|
+
|
|
89
|
+
**CSP:** a site that sets `form-action` must allow it: `form-action 'self' https://pit.world`.
|
|
90
|
+
Turn it off with `subscribe: false` (`--no-subscribe`). Installs before 0.3.0 cannot render the
|
|
91
|
+
form; their footer shows a plain "Newsletter" link to the same page instead, from the @latest template.
|
|
92
|
+
|
|
80
93
|
## CLI
|
|
81
94
|
|
|
82
95
|
```sh
|
|
@@ -20,6 +20,7 @@ const HELP = `usage: profullstack-footer <html|json|descriptor|template|check> [
|
|
|
20
20
|
--slug <slug> ring member slug (descriptor; default host with dashes)
|
|
21
21
|
--offline use the bundled template, do not fetch @latest
|
|
22
22
|
--no-css leave the <style> out
|
|
23
|
+
--no-subscribe leave the newsletter form out
|
|
23
24
|
`;
|
|
24
25
|
|
|
25
26
|
const { values, positionals } = parseArgs({
|
|
@@ -33,6 +34,7 @@ const { values, positionals } = parseArgs({
|
|
|
33
34
|
slug: { type: 'string' },
|
|
34
35
|
offline: { type: 'boolean' },
|
|
35
36
|
'no-css': { type: 'boolean' },
|
|
37
|
+
'no-subscribe': { type: 'boolean' },
|
|
36
38
|
help: { type: 'boolean', short: 'h' },
|
|
37
39
|
},
|
|
38
40
|
});
|
|
@@ -62,6 +64,7 @@ const opts = () => ({
|
|
|
62
64
|
since: values.since ? Number(values.since) : undefined,
|
|
63
65
|
tagline: values.tagline,
|
|
64
66
|
css: !values['no-css'],
|
|
67
|
+
subscribe: !values['no-subscribe'],
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
switch (cmd) {
|
package/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export interface FooterOptions {
|
|
|
20
20
|
houseLinks?: boolean;
|
|
21
21
|
/** CSP nonce for the inline <style> (style-src 'nonce-...'). */
|
|
22
22
|
nonce?: string;
|
|
23
|
+
/** false hides the newsletter form (it posts to SUBSCRIBE_URL; CSP form-action must allow https://pit.world). */
|
|
24
|
+
subscribe?: boolean;
|
|
23
25
|
}
|
|
24
26
|
export interface FooterTemplate {
|
|
25
27
|
version: string;
|
|
@@ -46,6 +48,8 @@ export interface DescriptorOptions {
|
|
|
46
48
|
|
|
47
49
|
export declare const RENDERER: number;
|
|
48
50
|
export declare const TEMPLATE_URL: string;
|
|
51
|
+
export declare const SUBSCRIBE_URL: string;
|
|
52
|
+
export declare function subscribeForm(site: string): string;
|
|
49
53
|
export declare const DEFAULT_RING: Required<RingOptions>;
|
|
50
54
|
export declare const DEFAULT_OWNER: { name: string; url: string };
|
|
51
55
|
export declare const bundledTemplate: FooterTemplate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/footer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One footer for every Profullstack site: links, copyright and the << Profullstack >> webring nav with Random. Sites render the @latest template from the CDN at runtime, so a release updates every site without a redeploy.",
|
|
6
6
|
"keywords": ["footer", "webring", "openwebring", "copyright", "react", "nextjs", "hono", "ssr"],
|
package/src/index.js
CHANGED
package/src/render.js
CHANGED
|
@@ -10,6 +10,11 @@ export const DEFAULT_RING = {
|
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_OWNER = { name: 'Profullstack, Inc.', url: 'https://profullstack.com/' };
|
|
12
12
|
|
|
13
|
+
// The one newsletter endpoint for every Profullstack property (double opt-in, keeps the site's host).
|
|
14
|
+
// Fixed here, never taken from the template: a CDN template cannot carry a <form> (sanitizeTemplate
|
|
15
|
+
// strips them), so it can only say WHERE the form goes ({{{subscribe}}}), not where it posts.
|
|
16
|
+
export const SUBSCRIBE_URL = 'https://pit.world/subscribe';
|
|
17
|
+
|
|
13
18
|
const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
14
19
|
|
|
15
20
|
/** @param {unknown} value */
|
|
@@ -48,6 +53,24 @@ export function ringLinks(site, ring = {}) {
|
|
|
48
53
|
};
|
|
49
54
|
}
|
|
50
55
|
|
|
56
|
+
/**
|
|
57
|
+
* The low-profile newsletter form: plain HTML, works with no script, posts the site's hostname so the
|
|
58
|
+
* signup records which property it came from. The endpoint answers with a "check your inbox" page and
|
|
59
|
+
* a link back here.
|
|
60
|
+
* @param {string} site
|
|
61
|
+
*/
|
|
62
|
+
export function subscribeForm(site) {
|
|
63
|
+
const host = new URL(canonicalSite(site)).hostname;
|
|
64
|
+
return (
|
|
65
|
+
`<form class="pfs-footer__sub" action="${SUBSCRIBE_URL}" method="post">` +
|
|
66
|
+
`<input type="hidden" name="site" value="${escapeHtml(host)}">` +
|
|
67
|
+
'<input class="pfs-footer__hp" name="website" type="text" tabindex="-1" autocomplete="off" aria-hidden="true">' +
|
|
68
|
+
'<input type="email" name="email" required maxlength="254" autocomplete="email" placeholder="Email for our newsletter" aria-label="Email address for the Profullstack newsletter">' +
|
|
69
|
+
'<button type="submit">Subscribe</button>' +
|
|
70
|
+
'</form>'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
/** @param {number} since @param {number} year */
|
|
52
75
|
export function years(since, year) {
|
|
53
76
|
return since && since < year ? `${since}–${year}` : String(year);
|
|
@@ -110,6 +133,7 @@ export function fill(tpl, vars) {
|
|
|
110
133
|
* css?: boolean,
|
|
111
134
|
* houseLinks?: boolean,
|
|
112
135
|
* nonce?: string,
|
|
136
|
+
* subscribe?: boolean,
|
|
113
137
|
* }} FooterOptions
|
|
114
138
|
* @typedef {{ version: string, renderer: number, css: string, link: string, footer: string, houseLinks?: FooterLink[] }} FooterTemplate
|
|
115
139
|
*/
|
|
@@ -143,6 +167,7 @@ export function renderFooter(template, opts) {
|
|
|
143
167
|
site: canonicalSite(opts.site),
|
|
144
168
|
name: opts.name ?? '',
|
|
145
169
|
nonce: opts.nonce ?? '',
|
|
170
|
+
subscribe: opts.subscribe === false ? '' : subscribeForm(opts.site),
|
|
146
171
|
});
|
|
147
172
|
}
|
|
148
173
|
|
package/src/template.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// render.js does not fill; older installs then keep their bundled template.
|
|
10
10
|
|
|
11
11
|
export default {
|
|
12
|
-
version: '0.
|
|
12
|
+
version: '0.3.0',
|
|
13
13
|
renderer: 1,
|
|
14
14
|
css: [
|
|
15
15
|
'.pfs-footer{--pfs-footer-muted:color-mix(in srgb,currentColor 62%,transparent);--pfs-footer-gap:.5rem 1.25rem;--pfs-footer-size:.875rem;',
|
|
@@ -22,6 +22,15 @@ export default {
|
|
|
22
22
|
'.pfs-footer__badges{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}',
|
|
23
23
|
'.pfs-footer a.pfs-footer__badge{display:inline-block;line-height:0}',
|
|
24
24
|
'.pfs-footer__badge img{display:block;width:200px;height:44px;max-width:100%}',
|
|
25
|
+
'.pfs-footer__news{display:flex;justify-content:center}',
|
|
26
|
+
'.pfs-footer__sub{display:flex;align-items:center;gap:.35rem;margin:0;flex-wrap:wrap;justify-content:center}',
|
|
27
|
+
'.pfs-footer__sub input[type=email]{font:inherit;font-size:.8125rem;color:inherit;background:transparent;width:13.5rem;max-width:62vw;padding:.2rem .5rem;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:.35rem}',
|
|
28
|
+
'.pfs-footer__sub input[type=email]::placeholder{color:var(--pfs-footer-muted);opacity:1}',
|
|
29
|
+
'.pfs-footer__sub input[type=email]:focus-visible{outline:2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset:1px}',
|
|
30
|
+
'.pfs-footer__sub button{font:inherit;font-size:.8125rem;color:var(--pfs-footer-muted);background:transparent;padding:.2rem .6rem;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:.35rem;cursor:pointer}',
|
|
31
|
+
'.pfs-footer__sub button:hover,.pfs-footer__sub button:focus-visible{color:inherit}',
|
|
32
|
+
'.pfs-footer__hp{position:absolute!important;left:-9999px!important;width:1px;height:1px;opacity:0}',
|
|
33
|
+
'.pfs-footer__sub+.pfs-footer__sublink{display:none}',
|
|
25
34
|
'.pfs-footer__copy{margin:0;color:var(--pfs-footer-muted)}',
|
|
26
35
|
'.pfs-footer__copy a{color:inherit}',
|
|
27
36
|
].join(''),
|
|
@@ -36,6 +45,9 @@ export default {
|
|
|
36
45
|
'<a href="{{ring}}">{{ring_name}}</a>',
|
|
37
46
|
'<a href="{{next}}" rel="next" title="Next site">>></a>',
|
|
38
47
|
'</nav>',
|
|
48
|
+
// The newsletter form comes from render.js ({{{subscribe}}}). Installs older than 0.3.0 cannot render
|
|
49
|
+
// it and show the plain link instead; when the form is there, CSS hides the link.
|
|
50
|
+
'<div class="pfs-footer__news">{{{subscribe}}}<a class="pfs-footer__sublink" href="https://pit.world/subscribe?site={{site}}">Newsletter</a></div>',
|
|
39
51
|
'<div class="pfs-footer__badges">',
|
|
40
52
|
'<a class="pfs-footer__badge" href="https://built.indie.pit.world/for?site={{site}}"><img src="https://built.indie.pit.world/for.svg?site={{site}}" alt="Listed on built.indie" width="200" height="44" loading="lazy"></a>',
|
|
41
53
|
'<a class="pfs-footer__badge pfs-footer__badge--pwamart" href="https://pwamart.com/for?site={{site}}"><img src="https://pwamart.com/for.svg?site={{site}}" alt="Get it on pwamart" width="200" height="44" loading="lazy"></a>',
|
package/template.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.3.0",
|
|
3
3
|
"renderer": 1,
|
|
4
|
-
"css": ".pfs-footer{--pfs-footer-muted:color-mix(in srgb,currentColor 62%,transparent);--pfs-footer-gap:.5rem 1.25rem;--pfs-footer-size:.875rem;border-top:1px solid color-mix(in srgb,currentColor 14%,transparent);font-size:var(--pfs-footer-size);line-height:1.5;color:inherit}.pfs-footer__inner{max-width:72rem;margin:0 auto;padding:1.5rem 1rem;display:flex;flex-direction:column;align-items:center;gap:.75rem;text-align:center}.pfs-footer a{color:var(--pfs-footer-muted);text-decoration:none}.pfs-footer a:hover,.pfs-footer a:focus-visible{color:inherit;text-decoration:underline}.pfs-footer__links{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--pfs-footer-gap)}.pfs-footer__ring{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}.pfs-footer__badges{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}.pfs-footer a.pfs-footer__badge{display:inline-block;line-height:0}.pfs-footer__badge img{display:block;width:200px;height:44px;max-width:100%}.pfs-footer__copy{margin:0;color:var(--pfs-footer-muted)}.pfs-footer__copy a{color:inherit}",
|
|
4
|
+
"css": ".pfs-footer{--pfs-footer-muted:color-mix(in srgb,currentColor 62%,transparent);--pfs-footer-gap:.5rem 1.25rem;--pfs-footer-size:.875rem;border-top:1px solid color-mix(in srgb,currentColor 14%,transparent);font-size:var(--pfs-footer-size);line-height:1.5;color:inherit}.pfs-footer__inner{max-width:72rem;margin:0 auto;padding:1.5rem 1rem;display:flex;flex-direction:column;align-items:center;gap:.75rem;text-align:center}.pfs-footer a{color:var(--pfs-footer-muted);text-decoration:none}.pfs-footer a:hover,.pfs-footer a:focus-visible{color:inherit;text-decoration:underline}.pfs-footer__links{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--pfs-footer-gap)}.pfs-footer__ring{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}.pfs-footer__badges{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}.pfs-footer a.pfs-footer__badge{display:inline-block;line-height:0}.pfs-footer__badge img{display:block;width:200px;height:44px;max-width:100%}.pfs-footer__news{display:flex;justify-content:center}.pfs-footer__sub{display:flex;align-items:center;gap:.35rem;margin:0;flex-wrap:wrap;justify-content:center}.pfs-footer__sub input[type=email]{font:inherit;font-size:.8125rem;color:inherit;background:transparent;width:13.5rem;max-width:62vw;padding:.2rem .5rem;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:.35rem}.pfs-footer__sub input[type=email]::placeholder{color:var(--pfs-footer-muted);opacity:1}.pfs-footer__sub input[type=email]:focus-visible{outline:2px solid color-mix(in srgb,currentColor 45%,transparent);outline-offset:1px}.pfs-footer__sub button{font:inherit;font-size:.8125rem;color:var(--pfs-footer-muted);background:transparent;padding:.2rem .6rem;border:1px solid color-mix(in srgb,currentColor 25%,transparent);border-radius:.35rem;cursor:pointer}.pfs-footer__sub button:hover,.pfs-footer__sub button:focus-visible{color:inherit}.pfs-footer__hp{position:absolute!important;left:-9999px!important;width:1px;height:1px;opacity:0}.pfs-footer__sub+.pfs-footer__sublink{display:none}.pfs-footer__copy{margin:0;color:var(--pfs-footer-muted)}.pfs-footer__copy a{color:inherit}",
|
|
5
5
|
"link": "<a href=\"{{href}}\"{{#rel}} rel=\"{{rel}}\"{{/rel}}>{{label}}</a>",
|
|
6
|
-
"footer": "<footer class=\"pfs-footer\" data-pfs-footer=\"{{version}}\">{{#css}}<style{{#nonce}} nonce=\"{{nonce}}\"{{/nonce}}>{{{css}}}</style>{{/css}}<div class=\"pfs-footer__inner\">{{#links}}<nav class=\"pfs-footer__links\" aria-label=\"Footer\">{{{links}}}</nav>{{/links}}<nav class=\"webring pfs-footer__ring\" aria-label=\"{{ring_name}} webring\"><a href=\"{{previous}}\" rel=\"prev\" title=\"Previous site\"><<</a><a href=\"{{ring}}\">{{ring_name}}</a><a href=\"{{next}}\" rel=\"next\" title=\"Next site\">>></a></nav><div class=\"pfs-footer__badges\"><a class=\"pfs-footer__badge\" href=\"https://built.indie.pit.world/for?site={{site}}\"><img src=\"https://built.indie.pit.world/for.svg?site={{site}}\" alt=\"Listed on built.indie\" width=\"200\" height=\"44\" loading=\"lazy\"></a><a class=\"pfs-footer__badge pfs-footer__badge--pwamart\" href=\"https://pwamart.com/for?site={{site}}\"><img src=\"https://pwamart.com/for.svg?site={{site}}\" alt=\"Get it on pwamart\" width=\"200\" height=\"44\" loading=\"lazy\"></a></div><p class=\"pfs-footer__copy\">© {{years}} <a href=\"{{owner_url}}\">{{owner_name}}</a>{{#tagline}} · {{tagline}}{{/tagline}}</p></div></footer>",
|
|
6
|
+
"footer": "<footer class=\"pfs-footer\" data-pfs-footer=\"{{version}}\">{{#css}}<style{{#nonce}} nonce=\"{{nonce}}\"{{/nonce}}>{{{css}}}</style>{{/css}}<div class=\"pfs-footer__inner\">{{#links}}<nav class=\"pfs-footer__links\" aria-label=\"Footer\">{{{links}}}</nav>{{/links}}<nav class=\"webring pfs-footer__ring\" aria-label=\"{{ring_name}} webring\"><a href=\"{{previous}}\" rel=\"prev\" title=\"Previous site\"><<</a><a href=\"{{ring}}\">{{ring_name}}</a><a href=\"{{next}}\" rel=\"next\" title=\"Next site\">>></a></nav><div class=\"pfs-footer__news\">{{{subscribe}}}<a class=\"pfs-footer__sublink\" href=\"https://pit.world/subscribe?site={{site}}\">Newsletter</a></div><div class=\"pfs-footer__badges\"><a class=\"pfs-footer__badge\" href=\"https://built.indie.pit.world/for?site={{site}}\"><img src=\"https://built.indie.pit.world/for.svg?site={{site}}\" alt=\"Listed on built.indie\" width=\"200\" height=\"44\" loading=\"lazy\"></a><a class=\"pfs-footer__badge pfs-footer__badge--pwamart\" href=\"https://pwamart.com/for?site={{site}}\"><img src=\"https://pwamart.com/for.svg?site={{site}}\" alt=\"Get it on pwamart\" width=\"200\" height=\"44\" loading=\"lazy\"></a></div><p class=\"pfs-footer__copy\">© {{years}} <a href=\"{{owner_url}}\">{{owner_name}}</a>{{#tagline}} · {{tagline}}{{/tagline}}</p></div></footer>",
|
|
7
7
|
"houseLinks": []
|
|
8
8
|
}
|