Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ authoritative record.
- **The gaps stay visible.** Missing dates, unmatched publication types and populated sections
omitted from a page appear in generated provenance instead of being silently invented.
- **It is a real site, not a CV pasted into HTML.** Blog posts, RSS, sitemap, search, dark mode,
responsive layouts, accessible navigation and GitHub Pages deployment are included.
responsive layouts, accessible navigation, social link previews and GitHub Pages deployment are
included.

The name is literal: a **ledger** is the record kept once and required to balance; the **press**
publishes it. The product is **ledgerpress**. **Ledger** is the design system inside it—the
Expand All @@ -96,7 +97,7 @@ high-contrast editorial interface in `web/`, including the Ledger Serif type fam
- **[Bundled example](https://eduardstan.github.io/ledgerpress/)**: The template's own fictional
palaeoclimatologist, Dr. Sahana Aster Kōwhai. Her record exercises cross-appointments, non-ASCII
names, talks, teaching tables, projects, service, fieldwork kept off the printed CV via
`printed: false`, a post and an SVG portrait.
`printed: false`, a post and a PNG portrait.
- **[eduardstan.github.io](https://eduardstan.github.io/)**: The real personal site and CV from
which ledgerpress was extracted.

Expand Down Expand Up @@ -291,6 +292,7 @@ something else.
- Add migration redirects in `web/src/lib/legacy-urls.ts`.
- Change or add website routes in `web/src/pages/`.
- Change announcement wording in the `TEMPLATES` table of `web/src/lib/announcements.ts`.
- Edit or delete the "Built with ledgerpress" credit line in `web/src/components/Footer.astro`.

None of them touches a fact about you, and taking a later ledgerpress update after one of them is a
merge you resolve rather than a conflict with your record.
Expand Down
11 changes: 7 additions & 4 deletions content/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ profile:
bluesky: # every other service takes a label and URL;
label: Bluesky # the site and PDF both render it without a
url: https://bsky.app/profile/ada.example # machinery edit.
portrait: portrait.jpg # a file in content/media/
portrait: portrait.jpg # a file in content/media/. A raster one also
# becomes the link-preview image; see `media/`.
favicon: favicon.svg # another file in content/media/. Optional: if it
# is omitted or missing, no icon link is emitted.
bio:
Expand Down Expand Up @@ -502,8 +503,10 @@ draft: false

Images. `profile.portrait` and the optional `profile.favicon` name files here by bare filename;
**inside a post or an item, refer to one as `/media/<file>`** — the directory is published at
that address. The build prefixes the project path from `profile.site` when the site is deployed
below an origin root.
that address. The portrait also supplies the Open Graph and Twitter card image when it is a
`.png`, `.jpg`, `.jpeg` or `.webp` file. Other formats, including SVG, still display on the site
but emit no social image tag. The build prefixes the project path from `profile.site` when the
site is deployed below an origin root.

## Announcements

Expand Down Expand Up @@ -544,7 +547,7 @@ Most fields reach both the website and the PDF. These land somewhere their names

| Field | Website | Printed CV |
| --- | --- | --- |
| `profile.site` | the origin and base path of every canonical, feed and sitemap URL | never read |
| `profile.site` | the origin and base path of every canonical, feed, sitemap and social-preview URL | never read |
| `profile.website` | not rendered | printed in the header contact line |
| `profile.bio.short` | shown on `/cv/` as "Short bio" | printed as "Short Bio" |
| `profile.bio.long` | the home page, in full, first sentence quoted above the fold | never printed |
Expand Down
2 changes: 1 addition & 1 deletion content/cv.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ profile:
bluesky:
label: Bluesky
url: https://bsky.app/profile/sahana-kowhai.example
portrait: portrait.svg
portrait: portrait.png
favicon: favicon.svg
bio:
short: >-
Expand Down
6 changes: 3 additions & 3 deletions content/media/LICENSES.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Example media

`portrait.svg`, `favicon.svg`, and `core-layers.svg` are original geometric illustrations created
for this ledgerpress example. They do not depict a real person and are licensed under the
repository's MIT licence.
`portrait.svg`, its raster `portrait.png` version, `favicon.svg`, and `core-layers.svg` are
original geometric illustrations created for this ledgerpress example. They do not depict a real
person and are licensed under the repository's MIT licence.

Replace this file with the provenance and licence terms for your own media when you adopt the
template.
Binary file added content/media/portrait.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion scripts/check-deployment-base.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,23 @@ try {
const text = (path) => readFileSync(join(proofDist, path), "utf8");
const home = text("index.html");
assert.match(home, /href="\/ledgerpress-proof\/publications\/"/);
assert.ok(home.includes(`property="og:url" content="${site}"`), "og:url lost deployment site base");
assert.ok(home.includes('property="og:type" content="website"'));
assert.ok(home.includes('name="twitter:card" content="summary"'));
if (portrait) {
assert.ok(home.includes(`src="${htmlAttribute(`${base}media/${portrait}`)}"`), `${portrait} lost the deployment base`);
if (/\.(?:png|jpe?g|webp)$/i.test(portrait)) {
assert.ok(home.includes(`property="og:image" content="${site}media/${portrait}"`), `og:image lost deployment site base`);
assert.ok(home.includes(`name="twitter:image" content="${site}media/${portrait}"`), `twitter:image lost deployment site base`);
} else {
assert.doesNotMatch(home, /property="og:image"/);
assert.doesNotMatch(home, /name="twitter:image"/);
}
}
const alternatePublications = text("publications/year-asc/index.html");
assert.ok(alternatePublications.includes(`rel="canonical" href="${site}publications/"`));
assert.doesNotMatch(alternatePublications, /property="og:/);
assert.doesNotMatch(alternatePublications, /name="twitter:/);
assert.match(home, /href="\/ledgerpress-proof\/fonts\/Archivo-Black\.woff2"/);
assert.ok(home.includes(`>${homeCounts}<`), `home count is not "${homeCounts}"`);

Expand Down Expand Up @@ -129,7 +143,7 @@ try {
"cv",
"404",
];
const unbased = new RegExp(`(?:src|href)="/(?!${base.slice(1)})(?:${prefixes.join("|")})(?:/|")`);
const unbased = new RegExp(`(?:src|href|content)="/(?!${base.slice(1)})(?:${prefixes.join("|")})(?:/|")`);
for (const page of pages) {
assert.doesNotMatch(text(page), unbased, `${page}: an internal URL lost the deployment base`);
}
Expand Down
7 changes: 6 additions & 1 deletion web/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,10 @@ const url = (path: string) => internalUrl(path, import.meta.env.BASE_URL);
</Record>
</div>
</div>
<p class="label colophon">&copy; {year} {name}</p>
{/* Adopters may delete or edit the template credit line below. */}
<p class="label colophon">
&copy; {year}{' '}{name} &middot; Built with <a href="https://github.com/eduardstan/ledgerpress"
>ledgerpress</a
>
</p>
</footer>
26 changes: 24 additions & 2 deletions web/src/layouts/BaseLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ interface Props {
/**
* Where this page's content really lives, when it is an alternate view of
* another page. One `<link rel="canonical">` is emitted either way: a second,
* conflicting one would make search engines ignore both.
* conflicting one would make search engines ignore both. Alternate views
* leave Open Graph and Twitter metadata to the canonical page.
*/
canonical?: string;
/** Keeps the page out of the Pagefind index — `<main>` is the indexed body. */
Expand All @@ -21,7 +22,7 @@ interface Props {

const { name } = profile();
const base = import.meta.env.BASE_URL;
const { favicon } = identity(base);
const { favicon, portrait } = identity(base);
const {
title,
description = `Personal site of ${name}.`,
Expand All @@ -30,6 +31,13 @@ const {
} = Astro.props;
const canonical = canonicalPath ? absoluteInternalUrl(canonicalPath, Astro.site!, base) : Astro.url;
const url = (path: string) => internalUrl(path, base);
/** Anything below the blog index is one post; the index itself is not. */
const blogIndex = url('/blog/');
const isPost = canonical.pathname.startsWith(blogIndex) && canonical.pathname !== blogIndex;
const socialImageUrl =
portrait && /\.(?:png|jpe?g|webp)$/i.test(portrait)
? new URL(portrait, Astro.site!).href
: undefined;
---

<html lang="en" data-theme="light">
Expand All @@ -39,6 +47,20 @@ const url = (path: string) => internalUrl(path, base);
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
{
canonicalPath === undefined && (
<>
<meta property="og:type" content={isPost ? 'article' : 'website'} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical.href} />
<meta property="og:site_name" content={name} />
<meta name="twitter:card" content="summary" />
{socialImageUrl && <meta property="og:image" content={socialImageUrl} />}
{socialImageUrl && <meta name="twitter:image" content={socialImageUrl} />}
</>
)
}
{favicon && <link rel="icon" href={favicon} />}
<link rel="alternate" type="application/rss+xml" title={name} href={url('/rss.xml')} />
{
Expand Down
Loading