@profullstack/footer 0.2.1 → 0.2.3

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 CHANGED
@@ -1,13 +1,13 @@
1
1
  # @profullstack/footer
2
2
 
3
3
  One footer for every Profullstack site: your links, the copyright line, and the
4
- `<< Profullstack >> ⚄` webring nav (previous, ring, next, random).
4
+ `<< Profullstack >>` webring nav (previous, ring, next).
5
5
 
6
6
  **Publish once, every site updates.** The markup and CSS ship as data
7
7
  (`template.json`). At render time each site fetches
8
8
  `https://cdn.jsdelivr.net/npm/@profullstack/footer@latest/template.json`
9
- (1 h cache, 1.5 s timeout, the bundled copy as the fallback), so a release reaches
10
- every site within the hour with no bump, rebuild or redeploy. Install it as
9
+ (15 min cache, 1.5 s timeout, the bundled copy as the fallback), so a release reaches
10
+ every site within about an hour (sooner on sites without a page cache) with no bump, rebuild or redeploy. Install it as
11
11
  `"@profullstack/footer": "latest"` too, so the render code follows on the next install.
12
12
 
13
13
  It renders on the server: the ring's verifier reads your HTML and never runs
@@ -83,7 +83,7 @@ switch (cmd) {
83
83
  const html = (await res.text()).replace(/&amp;/g, '&');
84
84
  const want = ringLinks(home);
85
85
  let ok = res.ok;
86
- for (const k of /** @type {const} */ (['previous', 'next', 'random'])) {
86
+ for (const k of /** @type {const} */ (['previous', 'next'])) {
87
87
  const has = html.includes(want[k]);
88
88
  ok &&= has;
89
89
  process.stdout.write(`${has ? 'ok ' : 'MISS'} ${k} ${want[k]}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/footer",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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
@@ -23,7 +23,10 @@ export { bundled as bundledTemplate };
23
23
 
24
24
  export const TEMPLATE_URL = 'https://cdn.jsdelivr.net/npm/@profullstack/footer@latest/template.json';
25
25
 
26
- const DEFAULT_TTL_MS = 60 * 60 * 1000;
26
+ // 15 min: a page cache (ISR, 1 h) and Next's fetch cache both sit in front of
27
+ // this one and each serves one stale copy before refreshing, so the layers add up.
28
+ const DEFAULT_TTL_MS = 15 * 60 * 1000;
29
+ const NEXT_REVALIDATE_S = 300;
27
30
  const FAILURE_TTL_MS = 5 * 60 * 1000;
28
31
 
29
32
  /** @type {{ template: any, until: number, pending: Promise<any> | null }} */
@@ -65,7 +68,7 @@ export async function latestTemplate(opts = {}) {
65
68
  const ctrl = typeof AbortController === 'function' ? new AbortController() : null;
66
69
  const timer = ctrl ? setTimeout(() => ctrl.abort(), opts.timeoutMs ?? 1500) : null;
67
70
  /** @type {RequestInit & { next?: { revalidate: number } }} */
68
- const init = { signal: ctrl?.signal, headers: { accept: 'application/json' }, next: { revalidate: Math.round(ttl / 1000) } };
71
+ const init = { signal: ctrl?.signal, headers: { accept: 'application/json' }, next: { revalidate: NEXT_REVALIDATE_S } };
69
72
  const res = await doFetch(url, init).finally(() => timer && clearTimeout(timer));
70
73
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
71
74
  const t = await res.json();
package/src/template.js CHANGED
@@ -8,20 +8,8 @@
8
8
  // Bump `renderer` only when the template needs placeholders an older
9
9
  // render.js does not fill; older installs then keep their bundled template.
10
10
 
11
- // OpenIcon line glyphs (github.com/profullstack/openicon, keys `random` and
12
- // `vote`) as CSS masks. A template may not carry <svg> (sanitizeTemplate strips
13
- // it), and a mask over currentColor keeps the icon the colour of the link.
14
- const glyph = (body) =>
15
- `url("data:image/svg+xml,${encodeURIComponent(
16
- `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'>${body}</svg>`,
17
- )}")`;
18
- const RANDOM = glyph(
19
- "<rect x='3' y='3' width='18' height='18' rx='3'/><circle cx='8' cy='8' r='1'/><circle cx='16' cy='8' r='1'/><circle cx='12' cy='12' r='1'/><circle cx='8' cy='16' r='1'/><circle cx='16' cy='16' r='1'/>",
20
- );
21
- const VOTE = glyph("<path d='M12 4 21 19H3z'/>");
22
-
23
11
  export default {
24
- version: '0.2.1',
12
+ version: '0.2.3',
25
13
  renderer: 1,
26
14
  css: [
27
15
  '.pfs-footer{--pfs-footer-muted:color-mix(in srgb,currentColor 62%,transparent);--pfs-footer-gap:.5rem 1.25rem;--pfs-footer-size:.875rem;',
@@ -31,10 +19,6 @@ export default {
31
19
  '.pfs-footer a:hover,.pfs-footer a:focus-visible{color:inherit;text-decoration:underline}',
32
20
  '.pfs-footer__links{display:flex;flex-wrap:wrap;justify-content:center;gap:var(--pfs-footer-gap)}',
33
21
  '.pfs-footer__ring{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:.6rem}',
34
- '.pfs-footer__icon{display:inline-block;width:1em;height:1em;vertical-align:-.125em;background-color:currentColor;',
35
- '-webkit-mask:var(--pfs-footer-glyph) center/contain no-repeat;mask:var(--pfs-footer-glyph) center/contain no-repeat}',
36
- `.pfs-footer__icon--random{--pfs-footer-glyph:${RANDOM}}`,
37
- `.pfs-footer__icon--vote{--pfs-footer-glyph:${VOTE}}`,
38
22
  '.pfs-footer__copy{margin:0;color:var(--pfs-footer-muted)}',
39
23
  '.pfs-footer__copy a{color:inherit}',
40
24
  ].join(''),
@@ -48,12 +32,6 @@ export default {
48
32
  '<a href="{{previous}}" rel="prev" title="Previous site">&lt;&lt;</a>',
49
33
  '<a href="{{ring}}">{{ring_name}}</a>',
50
34
  '<a href="{{next}}" rel="next" title="Next site">&gt;&gt;</a>',
51
- '<a href="{{random}}" title="Random site" aria-label="Random site"><span class="pfs-footer__icon pfs-footer__icon--random" aria-hidden="true"></span></a>',
52
- // Anonymous, throttled vote for this site. A bare URL because a new
53
- // placeholder would need a renderer bump; the ring host finds the site
54
- // from the Referer, and referrerpolicy keeps the origin on sites whose
55
- // own policy is no-referrer.
56
- '<a href="{{ring}}/vote" referrerpolicy="origin" title="Vote for this site" aria-label="Vote for this site"><span class="pfs-footer__icon pfs-footer__icon--vote" aria-hidden="true"></span></a>',
57
35
  '</nav>',
58
36
  '<p class="pfs-footer__copy">&copy; {{years}} <a href="{{owner_url}}">{{owner_name}}</a>{{#tagline}} &middot; {{tagline}}{{/tagline}}</p>',
59
37
  '</div>',
package/template.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.2.1",
2
+ "version": "0.2.3",
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__icon{display:inline-block;width:1em;height:1em;vertical-align:-.125em;background-color:currentColor;-webkit-mask:var(--pfs-footer-glyph) center/contain no-repeat;mask:var(--pfs-footer-glyph) center/contain no-repeat}.pfs-footer__icon--random{--pfs-footer-glyph:url(\"data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Crect%20x%3D'3'%20y%3D'3'%20width%3D'18'%20height%3D'18'%20rx%3D'3'%2F%3E%3Ccircle%20cx%3D'8'%20cy%3D'8'%20r%3D'1'%2F%3E%3Ccircle%20cx%3D'16'%20cy%3D'8'%20r%3D'1'%2F%3E%3Ccircle%20cx%3D'12'%20cy%3D'12'%20r%3D'1'%2F%3E%3Ccircle%20cx%3D'8'%20cy%3D'16'%20r%3D'1'%2F%3E%3Ccircle%20cx%3D'16'%20cy%3D'16'%20r%3D'1'%2F%3E%3C%2Fsvg%3E\")}.pfs-footer__icon--vote{--pfs-footer-glyph:url(\"data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2024%2024'%20fill%3D'none'%20stroke%3D'black'%20stroke-width%3D'2'%20stroke-linecap%3D'round'%20stroke-linejoin%3D'round'%3E%3Cpath%20d%3D'M12%204%2021%2019H3z'%2F%3E%3C%2Fsvg%3E\")}.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__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\">&lt;&lt;</a><a href=\"{{ring}}\">{{ring_name}}</a><a href=\"{{next}}\" rel=\"next\" title=\"Next site\">&gt;&gt;</a><a href=\"{{random}}\" title=\"Random site\" aria-label=\"Random site\"><span class=\"pfs-footer__icon pfs-footer__icon--random\" aria-hidden=\"true\"></span></a><a href=\"{{ring}}/vote\" referrerpolicy=\"origin\" title=\"Vote for this site\" aria-label=\"Vote for this site\"><span class=\"pfs-footer__icon pfs-footer__icon--vote\" aria-hidden=\"true\"></span></a></nav><p class=\"pfs-footer__copy\">&copy; {{years}} <a href=\"{{owner_url}}\">{{owner_name}}</a>{{#tagline}} &middot; {{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\">&lt;&lt;</a><a href=\"{{ring}}\">{{ring_name}}</a><a href=\"{{next}}\" rel=\"next\" title=\"Next site\">&gt;&gt;</a></nav><p class=\"pfs-footer__copy\">&copy; {{years}} <a href=\"{{owner_url}}\">{{owner_name}}</a>{{#tagline}} &middot; {{tagline}}{{/tagline}}</p></div></footer>",
7
7
  "houseLinks": []
8
8
  }