Example Components
Site Footer
A footer for every page: secondary links and a copyright line. The links and the year are written at build time, so the footer is plain HTML and ships no JavaScript.
Usage
name is the copyright holder and defaults to "Example Site".
true
examples/components/site-footer/site-footer.html
html
<site-footer data-bascik-prop-name="Example Site" /> html examples/components/site-footer/site-footer.html
<!-- @bascik
A site footer: secondary links and a copyright line. The links live in links.ts next to this file, and the year is written at build time, so the footer ships no JavaScript.
@prop name - Copyright holder. Defaults to "Example Site".
-->
<style>
.footer {
margin-top: var(--space-6, 4rem);
border-top: 1px solid var(--color-border, currentColor);
color: var(--color-muted, inherit);
font-size: 0.9rem;
}
.row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-3, 1rem);
max-width: var(--content-width, 70rem);
margin-inline: auto;
padding: var(--space-4, 1.5rem) var(--space-3, 1rem);
}
/* Two classes outrank a site-wide rule such as `.content ul` or `.content p`, so the resets below always apply. */
.footer .links {
display: flex;
flex-wrap: wrap;
gap: var(--space-3, 1rem);
margin: 0;
padding: 0;
list-style: none;
}
.links li {
margin: 0;
}
.links a {
color: inherit;
}
.footer .copyright {
margin: 0;
}
</style>
<footer class="footer" data-testid="site-footer">
<div class="row">
<nav aria-label="Footer" data-testid="site-footer-nav">
<script data-bascik-build-once type="text/typescript">
import { footerLinks } from "./links.ts";
export default () => footerLinks();
</script>
</nav>
<p class="copyright">
©
<script data-bascik-build-once type="text/typescript">
export default () => String(new Date().getFullYear());
</script>
<span data-bascik-prop-name>Example Site</span>
</p>
</div>
</footer> Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.
links.ts
ts examples/components/site-footer/links.ts
// links.ts: the footer links. Edit this list for your site. site-footer.html prints it from a build script.
// The markup is built with html from @bascik/bascik/html, which escapes each label and href for where it lands and
// turns a javascript: link into about:invalid, so there is no escaper to write.
import { html, type SafeHtml } from "@bascik/bascik/html";
export interface FooterLink {
label: string;
href: string;
}
export const FOOTER_LINKS: FooterLink[] = [
{ label: "About", href: "/about" },
{ label: "Blog", href: "/blog/" },
{ label: "Feed", href: "/feed.xml" },
{ label: "Contact", href: "/contact" },
];
/** The footer link list. */
export function footerLinks(links: FooterLink[] = FOOTER_LINKS): SafeHtml {
const items = links.map(
(link) => html`<li><a href="${link.href}">${link.label}</a></li>`,
);
// An array of html values is inserted as it is: no .join("").
return html`<ul class="links">
${items}
</ul>`;
} How It Works
- The footer is the same on every page, so both of its build scripts are
data-bascik-build-once: each runs once, when Bascik registers the component, and every page shares the output. Compare the Site Header, whosedata-bascik-buildscript readsBASCIK_PAGE_PATHto mark a different link on each page. - The year comes from a build script, so it is correct as of the last build. Rebuild at least once a year, or print it with a client
<script>if the site is rarely rebuilt. Build script output is cached, so addsrc/components/site-footer/**toscripts.cache.excludeto print a new year when no file changed. - The
<nav aria-label="Footer">label keeps it distinct from the header's navigation for screen reader users.
Adapt It
- For columns of links, return several labeled lists from
footerLinks()and lay them out with a grid. - To repeat the header's links, import them from
../site-header/links.ts, or move one shared list tosrc/lib/and import it as@/lib/links.ts.