@profullstack/footer 0.3.0 → 0.3.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/README.md +1 -0
- package/index.d.ts +11 -0
- package/package.json +1 -1
- package/src/embed.js +1 -0
- package/src/index.js +1 -0
- package/src/render.js +26 -0
- package/src/template.js +4 -1
- package/template.json +3 -3
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ npx @profullstack/footer html --site https://example.com/ --link Docs=/docs
|
|
|
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
75
|
| `subscribe` | `true` | `false` hides the newsletter form |
|
|
76
|
+
| `badges` | `[]` | `{ href, src, alt, width?, height? }[]`: badges only this site shows, after the house badges |
|
|
76
77
|
| `nonce` | | CSP nonce for the inline `<style>`; or `css: false` and style `.pfs-footer*` yourself |
|
|
77
78
|
|
|
78
79
|
Theme it with `--pfs-footer-muted`, `--pfs-footer-gap` and `--pfs-footer-size` on
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export interface FooterLink { label: string; href: string; rel?: string }
|
|
2
|
+
export interface FooterBadge {
|
|
3
|
+
href: string;
|
|
4
|
+
/** Image URL (http(s) or root-relative). */
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
width?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
}
|
|
2
10
|
export interface RingOptions { slug?: string; name?: string; host?: string }
|
|
3
11
|
export interface FooterOptions {
|
|
4
12
|
/** The site's homepage; hops are measured from it. */
|
|
@@ -22,6 +30,8 @@ export interface FooterOptions {
|
|
|
22
30
|
nonce?: string;
|
|
23
31
|
/** false hides the newsletter form (it posts to SUBSCRIBE_URL; CSP form-action must allow https://pit.world). */
|
|
24
32
|
subscribe?: boolean;
|
|
33
|
+
/** Badges only this site shows, after the house badges (e.g. a game's Foundplay badge). */
|
|
34
|
+
badges?: FooterBadge[];
|
|
25
35
|
}
|
|
26
36
|
export interface FooterTemplate {
|
|
27
37
|
version: string;
|
|
@@ -50,6 +60,7 @@ export declare const RENDERER: number;
|
|
|
50
60
|
export declare const TEMPLATE_URL: string;
|
|
51
61
|
export declare const SUBSCRIBE_URL: string;
|
|
52
62
|
export declare function subscribeForm(site: string): string;
|
|
63
|
+
export declare function siteBadges(badges?: FooterBadge[]): string;
|
|
53
64
|
export declare const DEFAULT_RING: Required<RingOptions>;
|
|
54
65
|
export declare const DEFAULT_OWNER: { name: string; url: string };
|
|
55
66
|
export declare const bundledTemplate: FooterTemplate;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/footer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
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/embed.js
CHANGED
package/src/index.js
CHANGED
package/src/render.js
CHANGED
|
@@ -71,6 +71,29 @@ export function subscribeForm(site) {
|
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
const SAFE_URL = /^\s*(https?:\/\/|\/(?!\/))/i;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Badges only this site shows, after the house badges: an image link each. Only http(s) or
|
|
78
|
+
* root-relative URLs; anything else is dropped.
|
|
79
|
+
* @param {FooterBadge[] | undefined} badges
|
|
80
|
+
*/
|
|
81
|
+
export function siteBadges(badges) {
|
|
82
|
+
return (badges ?? [])
|
|
83
|
+
.filter((b) => b && b.href && b.src && SAFE_URL.test(b.href) && SAFE_URL.test(b.src))
|
|
84
|
+
.map((b) => {
|
|
85
|
+
const size = ['width', 'height']
|
|
86
|
+
.filter((k) => Number.isFinite(Number(b[k])) && Number(b[k]) > 0)
|
|
87
|
+
.map((k) => ` ${k}="${Math.round(Number(b[k]))}"`)
|
|
88
|
+
.join('');
|
|
89
|
+
return (
|
|
90
|
+
`<a class="pfs-footer__badge pfs-footer__badge--site" href="${escapeHtml(b.href)}">` +
|
|
91
|
+
`<img src="${escapeHtml(b.src)}" alt="${escapeHtml(b.alt ?? '')}"${size} loading="lazy"></a>`
|
|
92
|
+
);
|
|
93
|
+
})
|
|
94
|
+
.join('');
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
/** @param {number} since @param {number} year */
|
|
75
98
|
export function years(since, year) {
|
|
76
99
|
return since && since < year ? `${since}–${year}` : String(year);
|
|
@@ -121,6 +144,7 @@ export function fill(tpl, vars) {
|
|
|
121
144
|
|
|
122
145
|
/**
|
|
123
146
|
* @typedef {{ label: string, href: string, rel?: string }} FooterLink
|
|
147
|
+
* @typedef {{ href: string, src: string, alt: string, width?: number, height?: number }} FooterBadge
|
|
124
148
|
* @typedef {{
|
|
125
149
|
* site: string,
|
|
126
150
|
* name?: string,
|
|
@@ -134,6 +158,7 @@ export function fill(tpl, vars) {
|
|
|
134
158
|
* houseLinks?: boolean,
|
|
135
159
|
* nonce?: string,
|
|
136
160
|
* subscribe?: boolean,
|
|
161
|
+
* badges?: FooterBadge[],
|
|
137
162
|
* }} FooterOptions
|
|
138
163
|
* @typedef {{ version: string, renderer: number, css: string, link: string, footer: string, houseLinks?: FooterLink[] }} FooterTemplate
|
|
139
164
|
*/
|
|
@@ -168,6 +193,7 @@ export function renderFooter(template, opts) {
|
|
|
168
193
|
name: opts.name ?? '',
|
|
169
194
|
nonce: opts.nonce ?? '',
|
|
170
195
|
subscribe: opts.subscribe === false ? '' : subscribeForm(opts.site),
|
|
196
|
+
badges: siteBadges(opts.badges),
|
|
171
197
|
});
|
|
172
198
|
}
|
|
173
199
|
|
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.3.
|
|
12
|
+
version: '0.3.1',
|
|
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,7 @@ 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__badge--site img{width:auto;height:auto}',
|
|
25
26
|
'.pfs-footer__news{display:flex;justify-content:center}',
|
|
26
27
|
'.pfs-footer__sub{display:flex;align-items:center;gap:.35rem;margin:0;flex-wrap:wrap;justify-content:center}',
|
|
27
28
|
'.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}',
|
|
@@ -51,6 +52,8 @@ export default {
|
|
|
51
52
|
'<div class="pfs-footer__badges">',
|
|
52
53
|
'<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>',
|
|
53
54
|
'<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>',
|
|
55
|
+
// Per-site badges (the `badges` option, e.g. a game's Foundplay badge), built and escaped by render.js.
|
|
56
|
+
'{{{badges}}}',
|
|
54
57
|
'</div>',
|
|
55
58
|
'<p class="pfs-footer__copy">© {{years}} <a href="{{owner_url}}">{{owner_name}}</a>{{#tagline}} · {{tagline}}{{/tagline}}</p>',
|
|
56
59
|
'</div>',
|
package/template.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.3.
|
|
2
|
+
"version": "0.3.1",
|
|
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__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}",
|
|
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__badge--site img{width:auto;height:auto}.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__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
|
|
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>{{{badges}}}</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
|
}
|