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">
<div class="row">
<nav aria-label="Footer">
<script data-bascik-build-once>
import { footerLinks } from "./links.ts";
export default () => footerLinks();
</script>
</nav>
<p class="copyright">
©
<script data-bascik-build>
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.
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" },
];
const ENTITIES: Record<string, string> = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
const escapeHtml = (value: string): string =>
value.replace(/[&<>"']/g, (ch) => ENTITIES[ch]);
/** The footer link list. */
export function footerLinks(links: FooterLink[] = FOOTER_LINKS): string {
const items = links.map(
(link) =>
`<li><a href="${escapeHtml(link.href)}">${escapeHtml(link.label)}</a></li>`,
);
return `<ul class="links">${items.join("")}</ul>`;
} How It Works
- The footer is the same on every page, but its build scripts still run in the context of each use. Their output is identical because it does not depend on the current page. Compare the Site Header, whose build script reads
BASCIK_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. - 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.