Compare commits

...

15 commits

Author SHA1 Message Date
Ben Aultowski
da66707338 Frozen in amber
Some checks are pending
Publish preview / build (push) Waiting to run
Release / release (push) Waiting to run
Release / notify-downstream (push) Blocked by required conditions
2026-02-16 20:16:27 -05:00
Jon McClure
1d7d05aea3
Merge pull request #393 from reuters-graphics/changeset-release/main
Version Packages
2026-02-16 09:04:46 +00:00
github-actions[bot]
4b74db1dc8 RELEASING: Releasing 1 package(s)
Releases:
  @reuters-graphics/graphics-components@3.1.0

[skip ci]
2026-02-16 08:54:14 +00:00
Jon McClure
760542b3a1
Merge pull request #392 from reuters-graphics/newsreader-font
Newsreader font
2026-02-16 08:53:30 +00:00
hobbes7878
43e00c8699
format 2026-02-16 08:50:09 +00:00
hobbes7878
0f9248c794
docs(changeset): Replaces FreightText with Newsreader Text font 2026-02-16 08:46:30 +00:00
hobbes7878
11d965ea4d
replaces freight text with newsreader text font 2026-02-16 08:45:54 +00:00
Jon McClure
54a7532ef4
Merge pull request #390 from reuters-graphics/changeset-release/main
Version Packages
2026-01-16 13:36:02 +00:00
github-actions[bot]
4f8d104d8a RELEASING: Releasing 1 package(s)
Releases:
  @reuters-graphics/graphics-components@3.0.27

[skip ci]
2026-01-16 13:35:26 +00:00
Jon McClure
14ab2d3e9e
Merge pull request #389 from reuters-graphics/jon-pkgprnew
Jon pkgprnew
2026-01-16 13:34:43 +00:00
hobbes7878
6dc6e54270
docs(changeset): Removes demo files, docs and stories from the published package. 2026-01-16 13:11:54 +00:00
hobbes7878
db2cf21eda
ignore docs and demo files from package 2026-01-16 12:20:24 +00:00
hobbes7878
f0889b805d
docs 2026-01-14 14:15:48 +00:00
hobbes7878
cbf0b11e32
contributing docs 2026-01-14 14:08:16 +00:00
hobbes7878
374c45fb5d
adds pkg.pr.new workflow 2026-01-14 11:31:05 +00:00
47 changed files with 1021 additions and 1184 deletions

6
.gitattributes vendored Normal file
View file

@ -0,0 +1,6 @@
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.flac filter=lfs diff=lfs merge=lfs -text
*.ait filter=lfs diff=lfs merge=lfs -text
*.ai filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text

32
.github/workflows/pkg.pr.new.yaml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Publish preview
on:
push:
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to pkg.pr.new
run: pnpm dlx pkg-pr-new publish --packageManager=pnpm

4
.gitignore vendored
View file

@ -188,6 +188,10 @@ dist
# SvelteKit build / generate output
.svelte-kit
/build
# Copied story assets (generated by scripts/copy-assets.js)
/static/stories
# End of https://www.toptal.com/developers/gitignore/api/node,macos,linux

View file

@ -1,5 +1,17 @@
# @reuters-graphics/graphics-components
## 3.1.0
### Minor Changes
- 0f9248c: Replaces FreightText with Newsreader Text font
## 3.0.27
### Patch Changes
- 6dc6e54: Removes demo files, docs and stories from the published package.
## 3.0.26
### Patch Changes

View file

@ -1,5 +1,3 @@
![](https://graphics.thomsonreuters.com/style-assets/images/logos/reuters-graphics-logo/svg/graphics-logo-color-dark.svg)
# Contributing Guide
## Why this?
@ -21,30 +19,48 @@ Anyone outside our team using these components is welcome to submit PRs or issue
We recommend your first step is to create an issue on this repo describing what is missing, broken or could be added or improved. (We'll close that issue when we merge your PR.)
- It's helpful if that issue describes what changes you propose to make at a high level so we can agree on a general direction before you write code. That's especially true if code you're writing will change how others need to write theirs.
- If needed, provide any links to best practice guidelines that support the change you want to make.
- If needed, provide any links to best practice guidelines that support the change you want to make, e.g., for making accessibility improvements to components.
- Tag others on the team who may have expertise or would contribute to any needed discussion.
- **Always tag an editor.**
### 🧹 Follow code standards
Once you're ready to submit code, be sure it's properly formatted _before_ you ask for a review. The easiest way is to ensure the built-in code formatter (prettier) is working. (It should.)
Once you're ready to submit code, be sure it's properly formatted _before_ you ask for a review and run our built-in code linters (eslint + prettier) over your changed files:
Be sure to add comments around any tricky bits of logic you're adding. Even better, document your code using [JSDoc](https://devhints.io/jsdoc) comments. (Check out [JSDoc shortcuts](https://code.visualstudio.com/docs/languages/javascript#_jsdoc-support) in VS Code for a leg-up.)
```console
pnpm lint && pnpm format
```
### 📝 Write docs
Any public methods or component props should be properly typed and documented with comments. (See existing components for examples.) For future developers, also add comments around any tricky bits of _internal_ logic you're adding.
All new components and component features should be reflected in a [docs page](https://reuters-graphics.github.io/graphics-svelte-components/) included with your PR. See the [README](https://github.com/reuters-graphics/graphics-svelte-components#developing-new-components) for instructions on how to add those docs.
### 📝 Write Storybook stories
### 🍺 Submit code
All new components and component features should be accompanied by [Storybook stories](https://reuters-graphics.github.io/graphics-svelte-components/) included with your PR. See other components for examples of how to add them.
Be sure to target your Storybook docs for non-developers with real world examples of how to use components within the graphics kit.
### 🍺 Submit code with a changeset
All code contributions should be made through the normal [GitHub Flow](https://www.w3schools.com/git/git_github_flow.asp#:~:text=The%20GitHub%20flow%20is%20a,Make%20changes%20and%20add%20Commits). Basically, make a branch and submit a pull request.
Generally, it's better to avoid bundling several new features or components in a single PR. Breaking them apart into smaller, individual contributions makes them easier to review and manage.
(Generally, it's better to avoid bundling several new features or components in a single PR. Breaking them apart into smaller, individual contributions makes them easier to review and manage.)
Each PR should be accompanied by a [changeset](https://github.com/changesets/changesets). You can add one by running:
```console
pnpm changeset add
```
Once you've submitted your PR, tag an editor to review it.
An editor will approve your PR after addressing any issues they see. Once an editor approves and there are no code conflicts, you can merge your PR into master.
An editor will approve your PR after addressing any issues they see. Once an editor approves and there are no conflicts or failing tests, you can merge your PR into master.
### Test in downstream projects
Once a PR is created, a testable version of the library is published via [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new). A comment will be added to your PR that documents how to install the library's test version. Use it to test any new components or features in the graphics kit.
### ✉️ Publishing to the team
Publishing is handled via [changesets](https://github.com/changesets/changesets) and should follow [semantic versioning](https://semver.org/) conventions. Most MINOR and all MAJOR version changes should be identified ahead of time during PR review.
Once a new version of the library is published, a [PR will be created in the graphics kit](https://github.com/reuters-graphics/bluprint_graphics-kit/pulls) to update this dependency. Merge that.

View file

@ -0,0 +1,23 @@
headline: Hello World
dek: Is this thing on?
section: Test
publishTime: 2026-02-16T12:00:00Z
[+body]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sem nulla, interdum eu justo et, scelerisque vehicula mi. Vivamus turpis purus, elementum eu iaculis eget, finibus varius est. In rhoncus eros id dolor tempor efficitur. Fusce nec bibendum sapien. In in mattis felis. Fusce nec massa faucibus, gravida libero vel, ultricies justo. Vivamus sagittis dui eget urna pharetra, ut convallis leo viverra. Integer a risus viverra, tristique enim nec, gravida ligula. Ut id sem non ligula sodales venenatis.
Curabitur eu gravida neque. Proin tempus dui non fringilla interdum. Morbi dui sem, auctor ac condimentum vestibulum, vehicula eu nunc. Nunc pellentesque sit amet massa in mattis. Nulla tincidunt arcu sed nulla bibendum, ut lobortis neque luctus. Etiam auctor imperdiet mattis. Aliquam pretium accumsan vestibulum. Nulla facilisi.
Pellentesque commodo ullamcorper felis sed semper. Sed id metus sed tortor cursus molestie id id purus. Curabitur vestibulum, nisi in iaculis venenatis, leo massa mattis urna, ac mollis lacus est eu ipsum. In eget urna diam. Mauris vehicula molestie risus in molestie. Nullam erat erat, auctor ac ligula vel, iaculis venenatis neque. Sed rutrum suscipit purus. Phasellus id nulla id mi suscipit faucibus sed in leo. Quisque dictum justo sit amet tellus venenatis, ut tristique justo volutpat. Curabitur quis purus sapien. Nam blandit congue sem, in feugiat justo fermentum quis. Aliquam sagittis vehicula nulla, quis tincidunt neque fringilla ut.
Sed consequat dui nec elit consequat semper. Ut nec sapien interdum, suscipit ante et, porta odio. Cras a mattis ligula, vitae euismod urna. Aenean auctor felis quis lacus semper, in rutrum quam blandit. Pellentesque aliquet quis dolor id maximus. Donec porttitor varius imperdiet. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent egestas vitae neque fermentum tempor. Vivamus quis scelerisque sapien. Proin ac nisl tristique, porta arcu quis, ullamcorper felis. Mauris et mi ipsum. Vestibulum molestie eget velit vitae fermentum.
Nam facilisis rutrum dolor nec pretium. Nam finibus tristique dolor at cursus. Fusce est massa, dictum a hendrerit ut, sollicitudin ac nunc. Curabitur tincidunt scelerisque auctor. Pellentesque congue erat at purus molestie pretium. Ut eros augue, posuere a dolor eu, convallis aliquet erat. Cras eu nibh sed nulla imperdiet ullamcorper. Cras ut malesuada nisl, at gravida sapien. Aliquam vitae justo at tortor fringilla elementum. Etiam aliquam purus ut dolor rhoncus, ac tempor elit feugiat. Donec maximus elit vestibulum odio ullamcorper, sed gravida enim semper. Cras dignissim iaculis odio, non posuere justo imperdiet nec. Vivamus finibus scelerisque ornare. Suspendisse nunc tortor, condimentum quis eros non, facilisis blandit diam. Ut ac varius nisl, at viverra odio. Vivamus enim orci, rutrum sit amet ex nec, feugiat sollicitudin metus.
[/body]
[endNotes]
title: End Note
text: Get wrecked, nerd!
[/endNotes]

View file

@ -1,43 +1,27 @@
{
"name": "@reuters-graphics/graphics-components",
"version": "3.0.26",
"name": "hypnagaga",
"version": "0.1.0",
"type": "module",
"private": false,
"homepage": "https://reuters-graphics.github.io/graphics-components",
"repository": {
"type": "git",
"url": "git+https://github.com/reuters-graphics/graphics-components.git"
},
"private": true,
"packageManager": "pnpm@9.13.2",
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"scripts": {
"start": "storybook dev -p 3000",
"dev": "svelte-kit sync && vite dev",
"build": "svelte-kit sync && node scripts/copy-assets.js && vite build",
"preview": "vite preview",
"storybook": "svelte-kit sync && storybook dev -p 3000",
"build:storybook": "svelte-kit sync && storybook build -o docs",
"lint": "eslint --fix",
"format": "prettier . --write",
"build": "rimraf ./dist && svelte-package -i ./src && publint",
"build:docs": "storybook build -o docs",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"changeset:version": "changeset version",
"changeset:publish": "git add --all && changeset publish",
"knip": "knip",
"test": "vitest"
},
"license": "MIT",
"files": [
"dist"
],
"engines": {
"node": ">=20.18"
},
"peerDependencies": {
"svelte": "^5.0.0"
},
"devDependencies": {
"@changesets/cli": "^2.29.2",
"@chromatic-com/storybook": "^3.2.6",
"@reuters-graphics/yaks-eslint": "^0.1.1",
"@reuters-graphics/yaks-prettier": "^0.1.1",
@ -52,7 +36,7 @@
"@storybook/sveltekit": "^8.6.12",
"@storybook/test": "^8.6.12",
"@storybook/theming": "^8.6.12",
"@sveltejs/package": "^2.3.11",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/css": "^0.0.37",
"@types/eslint": "^9.6.1",
@ -82,12 +66,10 @@
"prettier-plugin-svelte": "^3.3.3",
"prism-themes": "^1.9.0",
"prop-types": "^15.8.1",
"publint": "^0.3.12",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.1",
"rimraf": "^6.0.1",
"sass": "^1.86.3",
"storybook": "^8.6.12",
"svelte": "^5.28.1",
@ -100,6 +82,7 @@
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@lottiefiles/dotlottie-web": "^0.52.2",
"@reuters-graphics/svelte-markdown": "^0.0.3",
"@rferl/veronica": "github:rferl/veronica",
"@sveltejs/kit": "^2.0.0",
"dayjs": "^1.11.13",
"es-toolkit": "^1.35.0",
@ -113,18 +96,5 @@
"svelte-intersection-observer": "^1.0.0",
"ua-parser-js": "^2.0.3",
"vitest": "^3.2.4"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js",
"default": "./dist/index.js"
},
"./scss/*": "./dist/scss/*"
},
"svelte": "./dist/index.js",
"types": "./dist/index.d.ts",
"bugs": {
"url": "https://github.com/reuters-graphics/graphics-components/issues"
}
}

File diff suppressed because it is too large Load diff

61
scripts/copy-assets.js Normal file
View file

@ -0,0 +1,61 @@
/**
* Copies story assets (images, etc.) from content/stories/<slug>/
* into static/stories/<slug>/ so they're available as static files
* during the build.
*
* Run before `vite build`: node scripts/copy-assets.js
*/
import { readdirSync, cpSync, existsSync, mkdirSync, statSync } from 'node:fs';
import { join, resolve } from 'node:path';
const CONTENT_DIR = resolve('content/stories');
const STATIC_DIR = resolve('static/stories');
if (!existsSync(CONTENT_DIR)) {
console.log('No content/stories directory found, skipping asset copy.');
process.exit(0);
}
if (!existsSync(STATIC_DIR)) {
mkdirSync(STATIC_DIR, { recursive: true });
}
const slugs = readdirSync(CONTENT_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
let copied = 0;
for (const slug of slugs) {
const storyDir = join(CONTENT_DIR, slug);
const destDir = join(STATIC_DIR, slug);
const entries = readdirSync(storyDir, { withFileTypes: true });
for (const entry of entries) {
// Skip the .aml file itself
if (entry.name.endsWith('.aml')) continue;
const srcPath = join(storyDir, entry.name);
const destPath = join(destDir, entry.name);
if (!existsSync(destDir)) {
mkdirSync(destDir, { recursive: true });
}
if (entry.isDirectory()) {
cpSync(srcPath, destPath, { recursive: true });
copied++;
} else if (entry.isFile()) {
const srcStat = statSync(srcPath);
// Only copy if dest doesn't exist or is older
if (!existsSync(destPath) || statSync(destPath).mtimeMs < srcStat.mtimeMs) {
cpSync(srcPath, destPath);
copied++;
}
}
}
}
console.log(`Copied ${copied} asset(s) from ${slugs.length} story folder(s).`);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 472 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 131 B

View file

@ -1,197 +1,110 @@
<!-- @component `SEO` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-ads-analytic-seo--docs) -->
<!-- @component `SEO` — Provides page-level metadata, Open Graph, and JSON-LD structured data. -->
<script lang="ts">
interface GraphicAuthor {
interface Author {
name: string;
link: string;
link?: string;
}
interface Props {
/**
* Base url for the page, which in [Vite-based projects](https://vitejs.dev/guide/build.html#public-base-path) is globally available as `import.meta.env.BASE_URL`.
*/
baseUrl: string;
/**
* [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object for the page.
*/
pageUrl: URL;
/**
* SEO title
*/
/** Page title for search engines */
seoTitle: string;
/**
* SEO description
*/
/** Meta description */
seoDescription: string;
/**
* Share title
*/
shareTitle: string;
/**
* Share description
*/
shareDescription: string;
/**
* Share image path. **Must be an absolute path.**
*/
shareImgPath: string;
/**
* Share image alt text, up to 420 characters.
*/
/** Canonical URL for the page */
canonicalUrl?: string;
/** Title for social sharing (defaults to seoTitle) */
shareTitle?: string;
/** Description for social sharing (defaults to seoDescription) */
shareDescription?: string;
/** Absolute URL to share image */
shareImgPath?: string;
/** Alt text for share image */
shareImgAlt?: string;
/**
* Publish time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
*/
/** Publish time as ISO string */
publishTime?: string;
/**
* Updated time as an [ISO string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
*/
/** Updated time as ISO string */
updateTime?: string;
/**
* Array of authors for the piece. Each author object must have `name` and `link` attributes.
*/
authors?: GraphicAuthor[];
/** Authors */
authors?: Author[];
/** Site name for og:site_name */
siteName?: string;
}
let {
baseUrl,
pageUrl,
seoTitle,
seoDescription,
canonicalUrl = '',
shareTitle,
shareDescription,
shareImgPath,
shareImgPath = '',
shareImgAlt = '',
publishTime = '',
updateTime = '',
authors = [],
siteName = 'Hypnagaga',
}: Props = $props();
const getOrigin = (baseUrl: string) => {
try {
return new URL(baseUrl).origin;
} catch {
// This handles a weird case where Vite's base path is
// reset to './' after the app hydrates...
if (typeof window !== 'undefined') return getOrigin(window.location.href);
return '';
}
};
let origin = $derived(getOrigin(baseUrl));
let canonicalUrl = $derived(
(origin + (pageUrl?.pathname || '')).replace(/index\.html\/$/, '')
);
const orgLdJson = {
'@context': 'http://schema.org',
'@type': 'NewsMediaOrganization',
'@id': 'https://www.reuters.com/#publisher',
name: 'Reuters',
logo: {
'@type': 'ImageObject',
url: 'https://s3.reutersmedia.net/resources_v2/images/reuters_social_logo.png',
width: '200',
height: '200',
},
url: 'https://www.reuters.com/',
};
let ogTitle = $derived(shareTitle ?? seoTitle);
let ogDescription = $derived(shareDescription ?? seoDescription);
let articleLdJson = $derived({
'@context': 'http://schema.org',
'@type': 'NewsArticle',
'@type': 'Article',
headline: seoTitle,
url: canonicalUrl,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': canonicalUrl,
},
thumbnailUrl: shareImgPath,
image: [
{
'@context': 'http://schema.org',
'@type': 'ImageObject',
url: shareImgPath,
},
],
publisher: { '@id': 'https://www.reuters.com/#publisher' },
copyrightHolder: { '@id': 'https://www.reuters.com/#publisher' },
sourceOrganization: { '@id': 'https://www.reuters.com/#publisher' },
copyrightYear: new Date().getFullYear(),
dateCreated: publishTime,
datePublished: publishTime,
dateModified: updateTime,
url: canonicalUrl || undefined,
datePublished: publishTime || undefined,
dateModified: updateTime || publishTime || undefined,
author: authors.map(({ name, link }) => ({
'@type': 'Person',
name,
url: link,
...(link ? { url: link } : {}),
})),
creator: authors.map(({ name }) => name),
articleSection: 'Graphics',
isAccessibleForFree: true,
keywords: ['Reuters graphics', 'Reuters', 'graphics', 'Interactives'],
...(shareImgPath
? {
image: {
'@type': 'ImageObject',
url: shareImgPath,
},
}
: {}),
});
</script>
<svelte:head>
{#key canonicalUrl}
<title>{seoTitle}</title>
<meta name="description" content={seoDescription} />
{#if canonicalUrl}
<link rel="canonical" href={canonicalUrl} />
<link
rel="icon"
type="image/png"
href="https://graphics.thomsonreuters.com/style-assets/images/logos/favicon/favicon-96x96.png"
sizes="96x96"
/>
<link
rel="icon"
type="image/svg+xml"
href="https://graphics.thomsonreuters.com/style-assets/images/logos/favicon/kinesis.svg"
/>
<link
rel="shortcut icon"
type="image/x-icon"
href="https://graphics.thomsonreuters.com/style-assets/images/logos/favicon/favicon.ico"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="https://graphics.thomsonreuters.com/style-assets/images/logos/favicon/apple-touch-icon.png"
/>
{/if}
<meta property="og:url" content={canonicalUrl} />
<meta property="og:type" content="article" />
<meta property="og:title" content={shareTitle} itemprop="name" />
<meta
property="og:description"
content={shareDescription}
itemprop="description"
/>
<meta property="og:image" content={shareImgPath} itemprop="image" />
<meta property="og:site_name" content="Reuters" />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={ogDescription} />
{#if shareImgPath}
<meta property="og:image" content={shareImgPath} />
{/if}
<meta property="og:site_name" content={siteName} />
{#if canonicalUrl}
<meta property="og:url" content={canonicalUrl} />
{/if}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@ReutersGraphics" />
<meta name="twitter:creator" content="@ReutersGraphics" />
<meta name="twitter:domain" content={origin} />
<meta name="twitter:title" content={shareTitle} />
<meta name="twitter:description" content={shareDescription} />
<meta name="twitter:card" content={shareImgPath ? 'summary_large_image' : 'summary'} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
{#if shareImgPath}
<meta name="twitter:image" content={shareImgPath} />
{/if}
{#if shareImgAlt}
<meta name="twitter:image:alt" content={shareImgAlt} />
{/if}
<meta property="fb:app_id" content="319194411438328" />
<meta property="fb:admins" content="616167736" />
<meta property="fb:admins" content="625796953" />
<meta property="fb:admins" content="571759798" />
{#if publishTime}
<meta property="article:published_time" content={publishTime} />
{/if}
{#if updateTime}
<meta property="article:modified_time" content={updateTime} />
{/if}
<!-- svelte-ignore hydration_html_changed -->
{@html `<${'script'} type="application/ld+json">${JSON.stringify(
orgLdJson
)}</script>`}
<!-- svelte-ignore hydration_html_changed -->
{@html `<${'script'} type="application/ld+json">${JSON.stringify(
articleLdJson
)}</script>`}
{/key}
{@html `<${'script'} type="application/ld+json">${JSON.stringify(articleLdJson)}</script>`}
</svelte:head>

View file

@ -1,85 +1,59 @@
<!-- @component `SiteFooter` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-sitefooter--docs) -->
<!-- @component Simple site footer for Hypnagaga -->
<script lang="ts">
import QuickLinks from './QuickLinks.svelte';
import CompanyLinks from './CompanyLinks.svelte';
import LegalLinks from './LegalLinks.svelte';
import ReferralBlock from '../ReferralBlock/ReferralBlock.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import starterData from './data.json';
import { onMount } from 'svelte';
interface Props {
/**
* Set to `false` to remove graphics referrals
*/
includeReferrals?: boolean;
/** Copyright holder name */
name?: string;
}
let { includeReferrals = true }: Props = $props();
let { name = 'Hypnagaga' }: Props = $props();
let data = $state(starterData);
onMount(async () => {
if (new URL(document.location.href).origin !== 'https://www.reuters.com') {
return;
}
try {
const response = await fetch(
'https://www.reuters.com/site-api/footer/?' +
new URLSearchParams({
_website: 'reuters',
outputType: 'json',
})
);
const footerData = await response.json();
// Dumb verification...
if (!footerData[0].company_description) return;
data = footerData;
} catch {
console.warn('Unable to fetch site footer data');
}
});
const year = new Date().getFullYear();
</script>
<footer
class="my-0"
style={`
--nav-background: var(--theme-colour-background, #fff);
--nav-primary: var(--theme-colour-text-primary, #404040);
--nav-rules: var(--theme-colour-brand-rules, #d0d0d0);
--theme-font-family-sans-serif: Knowledge, sans-serif;
`}
>
<div>
{#if includeReferrals}
<PaddingReset>
<ReferralBlock
heading="More from Reuters Graphics"
collection="graphics"
class="fpy-4"
/>
</PaddingReset>
{/if}
<QuickLinks links={data[0]} />
<CompanyLinks links={data[0]} />
<LegalLinks links={data[0]} />
<footer>
<div class="footer-inner">
<p class="copyright">&copy; {year} {name}</p>
<nav class="footer-links">
<a href="/feed.xml">RSS</a>
</nav>
</div>
</footer>
<style lang="scss">
footer {
margin-block-start: 0;
background-color: var(--nav-background, #fff);
div {
max-width: 1400px;
border-top: 1px solid var(--theme-colour-brand-rules, #d0d0d0);
background-color: var(--theme-colour-background, #fff);
margin-top: 2rem;
}
.footer-inner {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
:global(a) {
.copyright {
margin: 0;
font-size: 0.85rem;
color: var(--theme-colour-text-secondary, #888);
}
.footer-links {
display: flex;
gap: 1rem;
a {
text-decoration: none;
font-size: 0.85rem;
color: var(--theme-colour-text-secondary, #888);
&:hover {
color: var(--theme-colour-text-primary, #222);
}
:global(a:hover) {
text-decoration: underline;
}
}
</style>

View file

@ -1,256 +1,90 @@
<!-- @component `SiteHeader` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-page-furniture-siteheader--docs) -->
<!-- @component Simple site header for Hypnagaga -->
<script lang="ts">
import ReutersLogo from '../ReutersLogo/ReutersLogo.svelte';
import NavBar from './NavBar/index.svelte';
import starterData from './data.json';
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import MenuIcon from './svgs/Menu.svelte';
import MobileMenu from './MobileMenu/index.svelte';
import type { Snippet } from 'svelte';
setContext('nav-active-section', writable<null | string>(null));
let data = $state(starterData);
let sections = $derived(data[0].sections);
let isMobileMenuOpen = $state(false);
onMount(async () => {
// Only fire on prod...
if (new URL(document.location.href).origin !== 'https://www.reuters.com') {
return;
interface Props {
/** Site title */
siteTitle?: string;
/** Navigation links */
links?: { href: string; label: string }[];
/** Optional custom content in the header */
children?: Snippet;
}
try {
const response = await fetch(
'https://www.reuters.com/site-api/header/?' +
new URLSearchParams({
_website: 'reuters',
outputType: 'json',
})
);
const headerData = await response.json();
// Dumb verification...
if (!headerData[0].sections) return;
data = headerData;
} catch {
console.warn('Unable to fetch site header data');
}
});
let {
siteTitle = 'Hypnagaga',
links = [
{ href: '/', label: 'Stories' },
{ href: '/feed.xml', label: 'RSS' },
],
children,
}: Props = $props();
</script>
<header
style={`
--nav-background: var(--theme-colour-background, #fff);
--nav-primary: var(--theme-colour-text-primary, #404040);
--nav-rules: var(--theme-colour-brand-rules, #d0d0d0);
--nav-accent: var(--theme-colour-brand-logo, #fa6400);
--nav-shadow: 0 1px 4px 2px var(--theme-colour-brand-shadow, rgb(255 255 255 / 10%));
--theme-font-family-sans-serif: Knowledge, sans-serif;
`}
>
<header>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div class="nav-container show-nav">
<div class="scroll-container">
<div class="inner">
<div class="main-bar">
<div class="logo-container">
<div class="logo">
<a href="https://www.reuters.com" aria-label="Reuters home">
<ReutersLogo
logoColour="var(--nav-accent)"
textColour="var(--nav-primary)"
/>
</a>
</div>
</div>
<NavBar {sections} />
<!-- Space takes the place of the MyViewMenu, NavSearchBar & Account components... -->
<div class="spacer-container">
<div class="spacer"></div>
</div>
<div class="mobile-button-group">
<div class="mobile-menu">
<button
class="menu-button"
aria-label="Menu"
aria-haspopup="true"
aria-expanded={isMobileMenuOpen}
onclick={() => {
isMobileMenuOpen = !isMobileMenuOpen;
}}
>
<div class="button-container">
<MenuIcon />
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<nav class="site-nav">
<a href="/" class="site-title">{siteTitle}</a>
<ul class="nav-links">
{#each links as link}
<li><a href={link.href}>{link.label}</a></li>
{/each}
</ul>
{#if children}
{@render children()}
{/if}
</nav>
</header>
<MobileMenu
{isMobileMenuOpen}
releaseMobileMenu={() => {
isMobileMenuOpen = false;
}}
data={data[0]}
/>
<style lang="scss">
@use './scss/_grids.scss' as grids;
@use './scss/_colors.scss' as *;
@use './scss/_eases.scss' as *;
@use './scss/_breakpoints.scss' as *;
@use './scss/_z-indexes.scss' as *;
$nav-height: 64px;
$mobile-nav-height: 56px;
$subnav-height: 48px;
.nav-container {
background-color: var(--nav-background, $white);
position: relative;
height: $nav-height;
z-index: $zindex-sticky;
--page-height: 0px;
@include for-tablet-down {
height: $mobile-nav-height;
header {
border-bottom: 1px solid var(--theme-colour-brand-rules, #d0d0d0);
background-color: var(--theme-colour-background, #fff);
position: sticky;
top: 0;
z-index: 100;
}
.site-nav {
max-width: 1200px;
margin: 0 auto;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.site-title {
font-size: 1.25rem;
font-weight: 700;
text-decoration: none;
color: var(--theme-colour-text-primary, #222);
letter-spacing: -0.01em;
&:hover {
opacity: 0.8;
}
}
.nav-links {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 1.25rem;
a {
text-decoration: none;
}
}
color: var(--theme-colour-text-secondary, #555);
font-size: 0.9rem;
.scroll-container {
height: calc(var(--page-height) - 50vh);
pointer-events: none;
.inner {
position: sticky;
top: 0;
background: var(--nav-background, $white);
pointer-events: auto;
border-bottom: 1px solid var(--nav-rules, var(--tr-muted-grey));
// @include for-tablet-down {
// border-bottom: none;
// transition: transform 0.25s $principleDefaultEase;
// transform: translateY(-100%);
// will-change: transform;
// }
}
}
.main-bar {
margin: 0 auto;
box-sizing: border-box;
display: flex;
height: $nav-height;
justify-content: space-between;
@include max-width;
@include grids.spacing-single(padding-inline-start padding-inline-end);
@include for-mobile {
height: $mobile-nav-height;
}
}
.logo-container {
align-self: center;
.logo {
display: block;
font-size: 0;
width: 126px;
min-width: 126px;
@media (max-width: 768px) {
width: 94px;
min-width: 94px;
&:hover {
color: var(--theme-colour-text-primary, #222);
}
}
}
.spacer-container {
margin-inline-start: auto;
display: flex;
align-items: center;
justify-content: flex-end;
@include for-mobile {
display: none;
}
.spacer {
width: 193.47px;
height: 64px;
@media (max-width: 1225px) {
width: 88px;
}
}
}
.mobile-button-group {
margin-inline-start: auto;
display: flex;
align-items: center;
justify-content: flex-end;
@include for-tablet-up {
display: none;
}
}
.mobile-menu {
margin-inline-start: 8px;
@include for-tablet-up {
display: none;
}
.menu-button {
width: 40px;
height: 40px;
display: inline-block;
vertical-align: top;
outline: none;
border: none;
margin: 0;
padding: 0;
overflow: visible;
background: transparent;
color: inherit;
font: inherit;
line-height: normal;
.button-container {
border-radius: 8px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
box-sizing: border-box;
border: 2px solid var(--nav-background);
}
&:hover .button-container {
box-shadow: var(--nav-shadow);
}
&:focus-visible .button-container {
border: 2px solid var(--nav-accent);
}
}
}
//Skip link styling. More about what a skip-link is and why we have it: https://www.w3schools.com/accessibility/accessibility_skip_links.php#:~:text=The%20HTML%20of%20a%20skip,to%20it%20with%20an%20anchor.
.skip-link {
position: absolute;
left: -10000px;
@ -258,11 +92,11 @@
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
&:focus {
position: static;
width: auto;
height: auto;
}
}
</style>

View file

@ -38,7 +38,7 @@ Use the [theme builder](?path=/docs/components-theming-theme-builder--docs) to s
base="dark"
theme={{
colour: { accent: 'var(--tr-light-orange)' },
font: { family: { hed: 'FreightText, serif' } },
font: { family: { hed: '"Newsreader Text", serif' } },
}}
>
<!-- Page content -->

View file

@ -37,7 +37,7 @@
base="dark"
theme={{
colour: { accent: 'var(--tr-light-orange)' },
font: { family: { hed: 'FreightText, serif' } },
font: { family: { hed: '"Newsreader Text", serif' } },
}}
>
<ThemedPage />
@ -100,7 +100,7 @@
<Theme
theme={{
colour: { background: 'steelblue', 'text-primary': '#fff' },
font: { family: { note: 'FreightText, serif' } },
font: { family: { note: '"Newsreader Text", serif' } },
}}
base="dark"
>

View file

@ -6,7 +6,7 @@ https://www.fluid-type-scale.com/calculate?minFontSize=18&minWidth=320&minRatio=
export default {
font: {
family: {
serif: 'FreightText, serif',
serif: '"Newsreader Text", serif',
'sans-serif': 'Knowledge, sans-serif',
monospace:
'"Droid Sans Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 131 B

198
src/lib/content.ts Normal file
View file

@ -0,0 +1,198 @@
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { parse } from '@rferl/veronica';
const CONTENT_DIR = resolve('content/stories');
// -- Types ------------------------------------------------------------------
export type ContainerWidth =
| 'narrower'
| 'narrow'
| 'normal'
| 'wide'
| 'wider'
| 'widest'
| 'fluid';
export interface TextBlock {
type: 'text';
value: string;
}
export interface PhotoBlock {
type: 'photo';
src: string;
alt: string;
caption?: string;
width?: ContainerWidth;
}
export interface GraphicBlock {
type: 'graphic';
title?: string;
description?: string;
notes?: string;
width?: ContainerWidth;
}
export type BodyBlock = TextBlock | PhotoBlock | GraphicBlock;
export interface EndNote {
title: string;
text: string;
}
export interface StoryMeta {
slug: string;
headline: string;
dek?: string;
section?: string;
publishTime: string;
updateTime?: string;
authors: string[];
coverImage?: string;
coverAlt?: string;
}
export interface StoryData extends StoryMeta {
body: BodyBlock[];
endNotes: EndNote[];
}
// -- Helpers ----------------------------------------------------------------
interface FreeformBlock {
type: string;
value: string | Record<string, string>;
}
function parseAuthors(raw: string | undefined): string[] {
if (!raw) return [];
return raw
.split(',')
.map((a) => a.trim())
.filter(Boolean);
}
function resolveImagePath(slug: string, src: string): string {
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) {
return src;
}
return `/stories/${slug}/${src}`;
}
function parseRaw(slug: string, raw: Record<string, unknown>): StoryData {
const body: BodyBlock[] = [];
const rawBody = (raw.body ?? []) as FreeformBlock[];
for (const block of rawBody) {
switch (block.type) {
case 'text':
body.push({ type: 'text', value: String(block.value ?? '') });
break;
case 'photo': {
const v = block.value as Record<string, string>;
body.push({
type: 'photo',
src: resolveImagePath(slug, v.src ?? ''),
alt: v.alt ?? '',
caption: v.caption,
width: (v.width as ContainerWidth) ?? undefined,
});
break;
}
case 'graphic': {
const v = block.value as Record<string, string>;
body.push({
type: 'graphic',
title: v.title,
description: v.description,
notes: v.notes,
width: (v.width as ContainerWidth) ?? undefined,
});
break;
}
default:
if (block.type) {
console.warn(`Unknown body block type: ${block.type}`);
}
}
}
const endNotes: EndNote[] = ((raw.endNotes ?? []) as Record<string, string>[]).map((n) => ({
title: n.title ?? '',
text: n.text ?? '',
}));
const coverImage = raw.coverImage
? resolveImagePath(slug, raw.coverImage as string)
: undefined;
return {
slug,
headline: (raw.headline as string) ?? 'Untitled',
dek: raw.dek as string | undefined,
section: raw.section as string | undefined,
publishTime: (raw.publishTime as string) ?? new Date().toISOString(),
updateTime: (raw.updateTime as string) || undefined,
authors: parseAuthors(raw.authors as string | undefined),
coverImage,
coverAlt: (raw.coverAlt as string) || undefined,
body,
endNotes,
};
}
// -- Public API -------------------------------------------------------------
export function listStories(): StoryMeta[] {
if (!existsSync(CONTENT_DIR)) return [];
const slugs = readdirSync(CONTENT_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
const stories: StoryMeta[] = [];
for (const slug of slugs) {
const amlPath = join(CONTENT_DIR, slug, 'story.aml');
if (!existsSync(amlPath)) continue;
const text = readFileSync(amlPath, 'utf-8');
const raw = parse(text) as Record<string, unknown>;
const data = parseRaw(slug, raw);
stories.push({
slug: data.slug,
headline: data.headline,
dek: data.dek,
section: data.section,
publishTime: data.publishTime,
updateTime: data.updateTime,
authors: data.authors,
coverImage: data.coverImage,
coverAlt: data.coverAlt,
});
}
return stories.sort(
(a, b) => new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()
);
}
export function loadStory(slug: string): StoryData | null {
const amlPath = join(CONTENT_DIR, slug, 'story.aml');
if (!existsSync(amlPath)) return null;
const text = readFileSync(amlPath, 'utf-8');
const raw = parse(text) as Record<string, unknown>;
return parseRaw(slug, raw);
}
export function getAllSlugs(): string[] {
if (!existsSync(CONTENT_DIR)) return [];
return readdirSync(CONTENT_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory() && existsSync(join(CONTENT_DIR, d.name, 'story.aml')))
.map((d) => d.name);
}

22
src/routes/+layout.svelte Normal file
View file

@ -0,0 +1,22 @@
<script lang="ts">
import Theme from '$lib/components/Theme/Theme.svelte';
import SiteHeader from '$lib/components/SiteHeader/SiteHeader.svelte';
import SiteFooter from '$lib/components/SiteFooter/SiteFooter.svelte';
import '$lib/scss/main.scss';
let { children } = $props();
</script>
<Theme base="light">
<SiteHeader />
<div id="main-content" class="site-content">
{@render children()}
</div>
<SiteFooter />
</Theme>
<style lang="scss">
.site-content {
min-height: 70vh;
}
</style>

1
src/routes/+layout.ts Normal file
View file

@ -0,0 +1 @@
export const prerender = true;

View file

@ -0,0 +1,7 @@
import { listStories } from '$lib/lib/content';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const stories = listStories();
return { stories };
};

127
src/routes/+page.svelte Normal file
View file

@ -0,0 +1,127 @@
<script lang="ts">
import Block from '$lib/components/Block/Block.svelte';
import { apdate } from 'journalize';
let { data } = $props();
</script>
<svelte:head>
<title>Hypnagaga</title>
<meta name="description" content="Stories and visual journalism" />
</svelte:head>
<main class="index">
<Block width="normal">
<header class="index-header">
<h1>Stories</h1>
</header>
{#if data.stories.length === 0}
<p class="empty">No stories yet.</p>
{:else}
<ul class="story-list">
{#each data.stories as story}
<li class="story-card">
<a href="/{story.slug}">
{#if story.coverImage}
<img
class="story-cover"
src={story.coverImage}
alt={story.coverAlt ?? story.headline}
loading="lazy"
/>
{/if}
<div class="story-info">
{#if story.section}
<span class="story-section">{story.section}</span>
{/if}
<h2 class="story-title">{story.headline}</h2>
{#if story.dek}
<p class="story-dek">{story.dek}</p>
{/if}
<time class="story-date" datetime={story.publishTime}>
{apdate(new Date(story.publishTime))}
</time>
</div>
</a>
</li>
{/each}
</ul>
{/if}
</Block>
</main>
<style lang="scss">
.index-header {
padding: 3rem 0 2rem;
h1 {
font-size: var(--text-4xl);
font-weight: 700;
margin: 0;
}
}
.empty {
padding: 2rem 0;
color: var(--color-text-secondary, #666);
}
.story-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 2.5rem;
padding-bottom: 4rem;
}
.story-card {
a {
text-decoration: none;
color: inherit;
display: block;
&:hover .story-title {
text-decoration: underline;
}
}
}
.story-cover {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 4px;
margin-bottom: 0.75rem;
}
.story-section {
display: inline-block;
font-size: var(--text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-accent, #d84315);
margin-bottom: 0.25rem;
}
.story-title {
font-size: var(--text-2xl);
font-weight: 700;
margin: 0 0 0.25rem;
line-height: 1.25;
}
.story-dek {
font-size: var(--text-base);
color: var(--color-text-secondary, #555);
margin: 0 0 0.5rem;
line-height: 1.5;
}
.story-date {
font-size: var(--text-sm);
color: var(--color-text-secondary, #888);
}
</style>

View file

@ -0,0 +1,15 @@
import { loadStory, getAllSlugs } from '$lib/lib/content';
import { error } from '@sveltejs/kit';
import type { PageServerLoad, EntryGenerator } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const story = loadStory(params.slug);
if (!story) {
error(404, `Story not found: ${params.slug}`);
}
return { story };
};
export const entries: EntryGenerator = async () => {
return getAllSlugs().map((slug) => ({ slug }));
};

View file

@ -0,0 +1,73 @@
<script lang="ts">
import Article from '$lib/components/Article/Article.svelte';
import Headline from '$lib/components/Headline/Headline.svelte';
import BodyText from '$lib/components/BodyText/BodyText.svelte';
import FeaturePhoto from '$lib/components/FeaturePhoto/FeaturePhoto.svelte';
import GraphicBlock from '$lib/components/GraphicBlock/GraphicBlock.svelte';
import EndNotes from '$lib/components/EndNotes/EndNotes.svelte';
let { data } = $props();
const story = data.story;
</script>
<svelte:head>
<title>{story.headline}</title>
{#if story.dek}
<meta name="description" content={story.dek} />
{/if}
<meta property="og:title" content={story.headline} />
{#if story.dek}
<meta property="og:description" content={story.dek} />
{/if}
{#if story.coverImage}
<meta property="og:image" content={story.coverImage} />
{/if}
<meta property="og:type" content="article" />
<meta property="article:published_time" content={story.publishTime} />
{#if story.updateTime}
<meta property="article:modified_time" content={story.updateTime} />
{/if}
</svelte:head>
<Article>
<Headline
hed={story.headline}
dek={story.dek}
section={story.section}
authors={story.authors}
publishTime={story.publishTime}
updateTime={story.updateTime}
/>
{#if story.coverImage}
<FeaturePhoto
src={story.coverImage}
altText={story.coverAlt ?? story.headline}
width="wide"
/>
{/if}
{#each story.body as block}
{#if block.type === 'text'}
<BodyText text={block.value} />
{:else if block.type === 'photo'}
<FeaturePhoto
src={block.src}
altText={block.alt}
caption={block.caption}
width={block.width ?? 'normal'}
/>
{:else if block.type === 'graphic'}
<GraphicBlock
title={block.title}
description={block.description}
notes={block.notes}
width={block.width ?? 'normal'}
/>
{/if}
{/each}
{#if story.endNotes.length > 0}
<EndNotes notes={story.endNotes} />
{/if}
</Article>

View file

@ -0,0 +1,47 @@
import { listStories } from '$lib/lib/content';
import type { RequestHandler } from './$types';
export const prerender = true;
export const GET: RequestHandler = async () => {
const stories = listStories();
const siteUrl = 'https://hypnagaga.com'; // Update with your actual URL
const siteTitle = 'Hypnagaga';
const siteDescription = 'Stories and visual journalism';
const items = stories
.map(
(story) => `
<item>
<title><![CDATA[${story.headline}]]></title>
<link>${siteUrl}/${story.slug}</link>
<guid isPermaLink="true">${siteUrl}/${story.slug}</guid>
${story.dek ? `<description><![CDATA[${story.dek}]]></description>` : ''}
<pubDate>${new Date(story.publishTime).toUTCString()}</pubDate>
${story.authors.length > 0 ? `<author>${story.authors.join(', ')}</author>` : ''}
${story.section ? `<category>${story.section}</category>` : ''}
</item>`
)
.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>${siteTitle}</title>
<description>${siteDescription}</description>
<link>${siteUrl}</link>
<atom:link href="${siteUrl}/feed.xml" rel="self" type="application/rss+xml"/>
<language>en-us</language>
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
${items}
</channel>
</rss>`;
return new Response(xml.trim(), {
headers: {
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=0, s-maxage=3600',
},
});
};

View file

@ -169,13 +169,13 @@
font-display: swap;
}
/* FREIGHT TEXT */
/* NEWSREADER TEXT */
@font-face {
font-family: 'FreightText';
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBold.woff2')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Bold.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBold.woff')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Bold.woff')
format('woff');
font-weight: bold;
font-style: normal;
@ -183,23 +183,119 @@
}
@font-face {
font-family: 'FreightText';
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBook.woff2')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraBoldItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBook.woff')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraBoldItalic.woff')
format('woff');
font-weight: normal;
font-weight: bold;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-BoldItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-BoldItalic.woff')
format('woff');
font-weight: bold;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraBold.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraBold.woff')
format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'FreightText';
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBookItalic.woff2')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-LightItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextBookItalic.woff')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-LightItalic.woff')
format('woff');
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-SemiBoldItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-SemiBoldItalic.woff')
format('woff');
font-weight: 600;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Medium.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Medium.woff')
format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-MediumItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-MediumItalic.woff')
format('woff');
font-weight: 500;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-SemiBold.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-SemiBold.woff')
format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraLightItalic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraLightItalic.woff')
format('woff');
font-weight: 200;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Italic.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Italic.woff')
format('woff');
font-weight: normal;
font-style: italic;
@ -207,25 +303,37 @@
}
@font-face {
font-family: 'FreightText';
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextLight.woff2')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Regular.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextLight.woff')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Regular.woff')
format('woff');
font-weight: 300;
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'FreightText';
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextMedium.woff2')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraLight.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/FreightTextMedium.woff')
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-ExtraLight.woff')
format('woff');
font-weight: 500;
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Newsreader Text';
src:
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Light.woff2')
format('woff2'),
url('//graphics.thomsonreuters.com/style-assets/fonts/v1/NewsreaderText-Light.woff')
format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}

View file

@ -27,8 +27,8 @@
.font-knowledge {
@include font-knowledge;
}
.font-freight-text {
@include font-freight-text;
.font-newsreader-text {
@include font-newsreader-text;
}
.font-noto-sans-jp {
@include font-noto-sans-jp;
@ -60,8 +60,8 @@
.\!font-knowledge {
@include \!font-knowledge;
}
.\!font-freight-text {
@include \!font-freight-text;
.\!font-newsreader-text {
@include \!font-newsreader-text;
}
.\!font-noto-sans-jp {
@include \!font-noto-sans-jp;

View file

@ -25,8 +25,8 @@
@mixin font-knowledge {
font-family: 'Knowledge', 'Source Sans Pro', Arial, Helvetica, sans-serif;
}
@mixin font-freight-text {
font-family: 'FreightText', serif;
@mixin font-newsreader-text {
font-family: 'Newsreader Text', serif;
}
@mixin font-noto-sans-jp {
font-family: 'Noto Sans JP';
@ -59,8 +59,8 @@
font-family:
'Knowledge', 'Source Sans Pro', Arial, Helvetica, sans-serif !important;
}
@mixin \!font-freight-text {
font-family: 'FreightText', serif !important;
@mixin \!font-newsreader-text {
font-family: 'Newsreader Text', serif !important;
}
@mixin \!font-noto-sans-jp {
font-family: 'Noto Sans JP' !important;

View file

@ -1,5 +1,3 @@
import slugify from 'slugify';
/** Helper function to generate a random 4-character string */
export const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
@ -8,10 +6,11 @@ export const random4 = () =>
/**
* Custom function that returns an author page URL.
* Returns an empty string by default (no link). Override in Byline's
* getAuthorPage prop if you want to link author names somewhere.
*/
export const getAuthorPageUrl = (author: string): string => {
const authorSlug = slugify(author.trim(), { lower: true });
return `https://www.reuters.com/authors/${authorSlug}/`;
export const getAuthorPageUrl = (_author: string): string => {
return '';
};
/** Formats a string containing a full or 3-letter abbreviated month, AM/PM, and am/pm to match the Reuters style.

View file

@ -1,21 +1,26 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
paths: {
base: '/graphics-components',
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: undefined,
precompress: false,
strict: true,
}),
files: {
lib: 'src',
},
alias: {
$lib: 'src',
'$lib/*': 'src/*',
$docs: 'src/docs',
'$docs/*': 'src/docs/*',
},
},
/** @type {import('@sveltejs/vite-plugin-svelte').SvelteConfig['onwarn']} */
onwarn: (warning, handler) => {
// Triggered by our use of SCSS mixins ...
if (warning.code === 'vite-plugin-svelte-preprocess-many-dependencies')
return;
handler(warning);

View file

@ -4,18 +4,14 @@
"lib": ["DOM", "ESNext"],
"module": "ESNext",
"target": "ESNext",
"declaration": true,
"isolatedModules": true,
"esModuleInterop": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"allowJs": true,
"checkJs": true,
"emitDeclarationOnly": true,
"jsx": "react",
"rootDir": ".",
"rootDirs": [".", "docs/docs-components"],
"outDir": "dist",
"sourceMap": true,
"strict": true,
"skipLibCheck": true,
@ -27,12 +23,11 @@
"src/**/*.ts",
"src/**/*.svelte",
"src/**/*.{jsx,tsx}",
"bin/**/*.{js,cjs}",
"*.ts",
"*.js",
"*.cjs",
"src/journalize.d.ts",
"src/docs/**/*.css"
],
"exclude": ["dist", "eslint.config.js"]
"exclude": ["build", "dist", "eslint.config.js"]
}