How-to
Build Scripts with Page Context
Some pages need content that is specific to the current page, such as a canonical URL in the head, an Open Graph image, a structured-data block, or even a page-specific sidebar. Hardcoding those values in every page file works. However, a shared script is easier to maintain. You can change the logic once and every page picks it up automatically.
Bascik makes this possible by injecting environment variables into every data-bascik-build subprocess:
| Variable | Value |
|---|---|
BASCIK_PAGE_PATH | Normalized root-relative route path of the page shell (e.g. /getting-started, /switch/from-vue, /) |
BASCIK_SOURCE_FILE | Absolute path to the file currently being transpiled (this is the file containing the executing <script data-bascik-build>, whether it's a page or a nested component template) |
BASCIK_PAGE_FILE | Absolute path to the HTML page currently being compiled (always points to the page shell, even when compiling a nested component) |
BASCIK_PAGES_DIR | Absolute path to the configured pages directory |
BASCIK_SITE_URL | The site URL from --site-url, the environment, or .env |
A build script reads these and computes whatever it needs, without the page knowing anything about the logic. See Environment Variables for the complete reference of all variables available in Bascik scripts.
Canonical URL Example
A canonical URL tag tells search engines which URL is the authoritative version of a page. Every docs page on this site uses a shared src/lib/canonical.ts that derives the URL from BASCIK_SOURCE_FILE (falling back to BASCIK_PAGE_FILE for legacy purposes):
// src/lib/canonical.ts
export async function canonical(): Promise<string> {
const siteUrl = (process.env.BASCIK_SITE_URL ?? "").replace(/\/$/, "");
const pageFile =
process.env.BASCIK_SOURCE_FILE ?? process.env.BASCIK_PAGE_FILE ?? "";
const pagesDir = process.env.BASCIK_PAGES_DIR ?? "";
if (!siteUrl || !pageFile || !pagesDir) return "";
// Convert the absolute file path to a root-relative route
const relPath = pageFile
.slice(pagesDir.length)
.replace(/^[\\/]/, "")
.replace(/\\/g, "/");
const withoutExt = relPath.replace(/\.html$/, "");
// index files map to the parent path
const route =
withoutExt === "index" ? "" : withoutExt.replace(/\/index$/, "/");
const urlPath = route ? `/${route}` : "/";
return `<link rel="canonical" href="${siteUrl}${urlPath}" />`;
} Use it from any page's <head>:
<head>
<script data-bascik-build>
import { canonical } from "@/lib/canonical.ts";
export default async () => await canonical();
</script>
</head> src/pages/getting-started.html emits:
<link rel="canonical" href="https://yourdomain.com/getting-started" /> src/pages/internals/architecture.html emits:
<link rel="canonical" href="https://yourdomain.com/internals/architecture" /> The script block is identical on every page. Bascik injects a different BASCIK_SOURCE_FILE for each, so the output is always specific to that page.
Reading the Page's Own HTML
For richer outputs, including Open Graph tags or JSON-LD structured data, a script can also read the page file itself to extract metadata. BASCIK_PAGE_FILE is an absolute path to the main page shell (and BASCIK_SOURCE_FILE is the file executing the script), so readFile works directly:
// src/lib/article-schema.ts (simplified)
import { readFile } from "node:fs/promises";
export async function articleSchema(): Promise<string> {
const pageFile =
process.env.BASCIK_SOURCE_FILE ?? process.env.BASCIK_PAGE_FILE ?? "";
const pagesDir = process.env.BASCIK_PAGES_DIR ?? "";
const siteUrl = (process.env.BASCIK_SITE_URL ?? "").replace(/\/$/, "");
if (!pageFile || !siteUrl) return "";
// Read the page's own source HTML to extract its title and description
const html = await readFile(pageFile, "utf8");
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/);
const descMatch = html.match(
/<meta\s+name="description"\s+content="([^"]+)"/,
);
if (!titleMatch || !descMatch) return "";
const headline = titleMatch[1].trim();
const description = descMatch[1].trim();
// ... compute url, build schema object, return JSON-LD script tag
} The script runs on the source HTML before any other build scripts have fired, so <title> and <meta name="description"> are always present as written.
One catch: build order. data-bascik-build scripts on the same page run in document order. If one script generates content that another script should read, such as a title generated by a build script, the second script will see the original source, not the output of the first. For metadata like <title> and <meta name="description"> that are hardcoded in the page, this is never an issue.
Component Build Scripts
<script data-bascik-build> inside a component runs when Bascik expands that component into a page. It receives that page's context, just like a build script written directly in the page. If a component appears more than once, each occurrence has its own build script output. When the script never reads the page context, Bascik reuses its output on other pages instead of running it again (see Output Caching).
That means shared components can derive navigation state, breadcrumbs, canonical links, or other output from the page path without a special marker. An imported helper can read BASCIK_PAGE_PATH directly:
<!-- src/components/docs-pagination/docs-pagination.html -->
<script data-bascik-build>
import { pagePath, siteOrigin } from "@/lib/site.ts";
export default () =>
`<link rel="canonical" href="${siteOrigin()}${pagePath()}">`;
</script> For output that is the same at every use of a component, use data-bascik-build-once instead:
<!-- src/components/site-footer/site-footer.html -->
<footer class="site-footer">
<p>
©
<script data-bascik-build-once>
export default () => String(new Date().getFullYear());
</script>
Example Co. All rights reserved.
</p>
</footer> It runs once while that component is registered, before page compilation, and every page that uses <site-footer /> gets the same © 2026 Example Co. line. It has no page or route context; the generated markup is still scoped and expanded normally. This is useful for shared navigation data, embedded assets, or footer values that do not vary by page. In a dynamic route template, it runs once for all of the template's routes. See data-bascik-build-once.
Use a pre exec script instead when expensive preparation should be shared across different components or at a project-wide scope. See Exec Scripts for lifecycle hooks.
Styling generated markup. Markup a component build script prints is scoped exactly like the component's template, including markup returned by an imported helper and class names built while the script runs. Classes and elements the component's stylesheet defines get scoped names, and classes it does not define stay global. Markup inside comments, <script>, <style>, and <textarea> is left as written.
The Pattern in Practice
Any tag or content block that should be consistent across every page but derived from per-page values is a good fit for this pattern:
| Output | What to read from the page |
|---|---|
<link rel="canonical"> | Derived from file path alone, with no file read needed |
| Open Graph tags | <title>, <meta name="description"> |
TechArticle JSON-LD | <title>, <meta name="description"> |
BreadcrumbList JSON-LD | File path, page <title> |
FAQPage JSON-LD | The content Markdown file, not the HTML page |
Write the helper once in src/lib/, call it from a page build script or a shared component build script, and every page gets the right output automatically.
The Site Header uses the current page path to mark the active link in its navigation. The Site Footer uses a build script too; because its output does not depend on the page, the result is the same at every use.
Next Steps: Learn how to test build helpers that read page context by mocking environment variables in the Build Scripts Testing Guide.
Next: Build Scripts covers the full set of env vars, caching, and shared script patterns.