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
4 changes: 3 additions & 1 deletion 404.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
table{border-collapse:collapse;width:100%;margin-top:8px;font-size:16px}
th,td{border-bottom:1px solid var(--line);text-align:left;padding:10px 8px;vertical-align:top}
</style>
<link rel="stylesheet" href="/assets/pages.css">
</head>
<body><main class="wrap">
<h1>404: page not found</h1>
Expand All @@ -42,5 +43,6 @@ <h1>404: page not found</h1>
- Full reference with prices and buy links: https://207studio.dev/llms-full.txt
- Contact: support@207studio.dev
</pre>
</main></body>
</main><footer class="site-footer page-foot-hand"><div class="foot-grid"><div class="foot-brand"><a class="brand" href="/">207 Studio</a><p>Software for making and teaching.</p></div><nav class="foot-col" aria-label="Work"><h2>Work</h2><a href="/products/">Products</a><a href="/blog/">Writing</a><a href="https://apps.apple.com/app/id6815584225" target="_blank" rel="noopener">Pairy</a></nav><nav class="foot-col" aria-label="Studio"><h2>Studio</h2><a href="/about/">About</a><a href="mailto:support@207studio.dev">Support</a><a href="/llms.txt">llms.txt</a></nav><nav class="foot-col" aria-label="Legal"><h2>Legal</h2><a href="/legal/privacy/">Privacy Policy</a><a href="/legal/terms/">Terms of Use</a><a href="/legal/refunds/">Refund Policy</a></nav></div><img class="foot-word" src="/assets/footer-207.svg" alt="" aria-hidden="true" width="1513" height="700" loading="lazy" decoding="async"><div class="foot-bottom"><span>© 2026 207 Studio · South Korea</span></div></footer>
</body>
</html>
2 changes: 2 additions & 0 deletions _blog/posts.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"pptx"
],
"image": "/assets/products/pptx-player-sdk-1200.jpg",
"image_width": 1200,
"image_height": 675,
"image_alt": "PPTX Player SDK diagram: a slide stepped with click 1, click 2, then a morph transition",
"image_caption": "A deck steps the way PowerPoint plays it: click 1, click 2, then a morph transition between slides."
}
Expand Down
36 changes: 26 additions & 10 deletions _products/blog-gen.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -192,26 +192,39 @@ const CSS = `:root{--bg:#fafbf8;--ink:#102b32;--muted:#52646a;--line:#d6dedb;--l
.post-list time{font-size:.88rem;color:var(--muted)}
`;

function head(title, description, canonical, ogImage, extraCss = '/blog/blog.css') {
const image = ogImage ? `<meta property="og:image" content="${esc(ogImage)}">\n<meta name="twitter:image" content="${esc(ogImage)}">\n` : '';
// opts.ko: the Korean twin's URL (adds hreflang). opts.cardImage: og/twitter image for pages without a hero image.
function head(title, description, canonical, ogImage, opts = {}) {
const extraCss = opts.extraCss || '/blog/blog.css';
const image = ogImage
? `<meta property="og:image" content="${esc(ogImage)}">\n<meta name="twitter:image" content="${esc(ogImage)}">\n`
: opts.cardImage ? `<meta property="og:image" content="${esc(opts.cardImage)}">\n` : '';
const twImage = ogImage || opts.cardImage;
const alternates = opts.ko
? `<link rel="alternate" hreflang="en" href="${esc(canonical)}">\n<link rel="alternate" hreflang="ko" href="${esc(opts.ko)}">\n<link rel="alternate" hreflang="x-default" href="${esc(canonical)}">\n`
: '';
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#102b32">
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' rx='9' fill='%23102b32'/%3E%3Cpath d='M10 12h20v4L20 29h-6l10-13H10z' fill='%23d5f66b'/%3E%3C/svg%3E">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/products/products.css">
<link rel="stylesheet" href="${extraCss}">
<title>${esc(title)}</title>
<meta name="description" content="${esc(description)}">
<link rel="canonical" href="${esc(canonical)}">
<meta property="og:title" content="${esc(title)}">
${alternates}<meta property="og:title" content="${esc(title)}">
<meta property="og:description" content="${esc(description)}">
<meta property="og:type" content="article">
<meta property="og:url" content="${esc(canonical)}">
${image}<meta property="og:site_name" content="207 Studio">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${esc(title)}">
<meta name="twitter:description" content="${esc(description)}">
${twImage ? `<meta name="twitter:image" content="${esc(twImage)}">\n` : ''}<link rel="stylesheet" href="/assets/pages.css">
</head>`;
}

Expand All @@ -220,8 +233,11 @@ function header(current) {
return `<header class="site-header wrap"><a class="brand" href="/"><span class="brand-mark">207</span><span>STUDIO</span></a><nav aria-label="Main navigation"><a href="/">Home</a><a href="/products/">Products</a><a href="/blog/"${blog}>Writing</a></nav></header>`;
}

function footer() {
return `<footer class="site-footer wrap"><p>207 Studio (207studio.dev), South Korea. Support: <a href="mailto:${esc(data.support_email)}">${esc(data.support_email)}</a></p><p>Last updated ${esc(data.last_updated)}. \u00a9 2026 207 Studio</p></footer>`;
const FOOT_GRID = '<div class="foot-grid"><div class="foot-brand"><a class="brand" href="/">207 Studio</a><p>Software for making and teaching.</p></div><nav class="foot-col" aria-label="Work"><h2>Work</h2><a href="/products/">Products</a><a href="/blog/">Writing</a><a href="https://apps.apple.com/app/id6815584225" target="_blank" rel="noopener">Pairy</a></nav><nav class="foot-col" aria-label="Studio"><h2>Studio</h2><a href="/about/">About</a><a href="mailto:support@207studio.dev">Support</a><a href="/llms.txt">llms.txt</a></nav><nav class="foot-col" aria-label="Legal"><h2>Legal</h2><a href="/legal/privacy/">Privacy Policy</a><a href="/legal/terms/">Terms of Use</a><a href="/legal/refunds/">Refund Policy</a></nav></div><img class="foot-word" src="/assets/footer-207.svg" alt="" aria-hidden="true" width="1513" height="700" loading="lazy" decoding="async">';

function footer(ko) {
const lang = ko ? `<a href="${esc(ko)}" lang="ko" hreflang="ko">\ud55c\uad6d\uc5b4</a>` : '';
return `<footer class="site-footer wrap">${FOOT_GRID}<div class="foot-bottom"><span>\u00a9 2026 207 Studio \u00b7 South Korea</span>${lang}</div></footer>`;
}

const scriptTag = (obj) =>
Expand Down Expand Up @@ -254,7 +270,7 @@ for (const post of posts) {

const graph = [
{
'@type': 'Article',
'@type': 'BlogPosting',
'@id': `${canonical}#article`,
headline: h1,
description: post.description,
Expand Down Expand Up @@ -288,7 +304,7 @@ for (const post of posts) {

const ogImage = post.image ? `${BASE}${post.image}` : '';
const figure = post.image
? `<figure class="post-figure"><img src="${esc(post.image)}" alt="${esc(post.image_alt || '')}" loading="eager">${post.image_caption ? `<figcaption>${inline(post.image_caption)}</figcaption>` : ''}</figure>`
? `<figure class="post-figure"><img src="${esc(post.image)}"${post.image_width ? ` width="${post.image_width}" height="${post.image_height}"` : ''} alt="${esc(post.image_alt || '')}" loading="eager">${post.image_caption ? `<figcaption>${inline(post.image_caption)}</figcaption>` : ''}</figure>`
: '';

const page = `${head(post.meta_title || post.title, post.description, canonical, ogImage)}
Expand Down Expand Up @@ -320,7 +336,7 @@ const listItems = posts
)
.join('\n');

const listPage = `${head('Writing \u2013 notes on browser rendering and classroom tools | 207 Studio', 'Practical notes from 207 Studio on rendering documents in the browser, offline classroom tools, and the code behind them.', `${BASE}/blog/`)}
const listPage = `${head('Writing \u2013 notes on browser rendering and classroom tools | 207 Studio', 'Practical notes from 207 Studio on rendering documents in the browser, offline classroom tools, and the code behind them.', `${BASE}/blog/`, '', { ko: `${BASE}/ko/blog/`, cardImage: posts.find((p) => p.image) ? `${BASE}${posts.find((p) => p.image).image}` : '' })}
<body>
${header('blog')}
<main class="wrap product-page blog-index">
Expand All @@ -331,7 +347,7 @@ ${header('blog')}
${listItems}
</ul>
</main>
${footer()}
${footer(`${BASE}/ko/blog/`)}
</body>
</html>
`;
Expand Down
36 changes: 24 additions & 12 deletions _products/gen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ struct Tier {
paddle_price_id: Option<String>,
}

const FAVICON: &str = "<link rel=\"icon\" type=\"image/svg+xml\" href=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' rx='9' fill='%23102b32'/%3E%3Cpath d='M10 12h20v4L20 29h-6l10-13H10z' fill='%23d5f66b'/%3E%3C/svg%3E\">";
const FAVICON: &str = "<link rel=\"icon\" href=\"/favicon.ico\" sizes=\"any\">\n<link rel=\"icon\" type=\"image/svg+xml\" href=\"/assets/logo.svg\">\n<link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\">";
// Footer columns shared with every inner page (assets/pages.css styles them).
const FOOT_GRID: &str = r##"<div class="foot-grid"><div class="foot-brand"><a class="brand" href="/">207 Studio</a><p>Software for making and teaching.</p></div><nav class="foot-col" aria-label="Work"><h2>Work</h2><a href="/products/">Products</a><a href="/blog/">Writing</a><a href="https://apps.apple.com/app/id6815584225" target="_blank" rel="noopener">Pairy</a></nav><nav class="foot-col" aria-label="Studio"><h2>Studio</h2><a href="/about/">About</a><a href="mailto:support@207studio.dev">Support</a><a href="/llms.txt">llms.txt</a></nav><nav class="foot-col" aria-label="Legal"><h2>Legal</h2><a href="/legal/privacy/">Privacy Policy</a><a href="/legal/terms/">Terms of Use</a><a href="/legal/refunds/">Refund Policy</a></nav></div><img class="foot-word" src="/assets/footer-207.svg" alt="" aria-hidden="true" width="1513" height="700" loading="lazy" decoding="async">"##;

fn main() {
let args: Vec<String> = env::args().collect();
Expand Down Expand Up @@ -173,6 +175,7 @@ fn head(
image_jpg: &str,
) -> String {
let image = esc(&absolute(&data.base_url, image_jpg));
let ko = esc(&ko_url(&data.base_url, canonical));
let canonical = esc(canonical);
let title = esc(title);
let description = esc(description);
Expand All @@ -188,13 +191,20 @@ fn head(
<title>{title}</title>\n\
<meta name=\"description\" content=\"{description}\">\n\
<link rel=\"canonical\" href=\"{canonical}\">\n\
<link rel=\"alternate\" hreflang=\"en\" href=\"{canonical}\">\n\
<link rel=\"alternate\" hreflang=\"ko\" href=\"{ko}\">\n\
<link rel=\"alternate\" hreflang=\"x-default\" href=\"{canonical}\">\n\
<meta property=\"og:title\" content=\"{title}\">\n\
<meta property=\"og:description\" content=\"{description}\">\n\
<meta property=\"og:type\" content=\"website\">\n\
<meta property=\"og:url\" content=\"{canonical}\">\n\
<meta property=\"og:image\" content=\"{image}\">\n\
<meta property=\"og:site_name\" content=\"207 Studio\">\n\
<meta name=\"twitter:card\" content=\"summary_large_image\">\n\
<meta name=\"twitter:title\" content=\"{title}\">\n\
<meta name=\"twitter:description\" content=\"{description}\">\n\
<meta name=\"twitter:image\" content=\"{image}\">\n\
<link rel=\"stylesheet\" href=\"/assets/pages.css\">\n\
</head>\n"
)
}
Expand All @@ -210,11 +220,16 @@ fn header(products_current: bool) -> String {
)
}

fn footer(data: &SiteData) -> String {
let email = esc(&data.support_email);
let updated = esc(&data.last_updated);
// The Korean twin of an English page: /products/x/ -> /ko/products/x/.
fn ko_url(base_url: &str, canonical: &str) -> String {
canonical.replacen(&format!("{base_url}/"), &format!("{base_url}/ko/"), 1)
}

fn footer(data: &SiteData, canonical: &str) -> String {
let ko = esc(&ko_url(&data.base_url, canonical));
let grid = FOOT_GRID.replace("support@207studio.dev", &esc(&data.support_email));
format!(
"<footer class=\"site-footer wrap\"><p>207 Studio (207studio.dev), South Korea. Support: <a href=\"mailto:{email}\">{email}</a></p><p>Last updated {updated}. © 2026 207 Studio</p></footer>\n"
"<footer class=\"site-footer wrap\">{grid}<div class=\"foot-bottom\"><span>© 2026 207 Studio · South Korea</span><a href=\"{ko}\" lang=\"ko\" hreflang=\"ko\">한국어</a></div></footer>\n"
)
}

Expand Down Expand Up @@ -244,11 +259,7 @@ fn offers(data: &SiteData, product: &Product) -> Vec<Value> {
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"url": tier.url,
"seller": {
"@type": "Organization",
"name": data.organization.name,
"url": data.organization.url,
},
"seller": { "@id": format!("{}/#org", data.organization.url) },
"shippingDetails": shipping_details(),
"hasMerchantReturnPolicy": return_policy(),
})
Expand Down Expand Up @@ -564,6 +575,7 @@ fn render_listing(data: &SiteData) -> Result<String, String> {
"name": org.name,
"url": org.url,
"email": org.email,
"logo": format!("{}/assets/logo.png", org.url),
"address": { "@type": "PostalAddress", "addressCountry": org.country },
"sameAs": org.same_as,
},
Expand All @@ -582,7 +594,7 @@ fn render_listing(data: &SiteData) -> Result<String, String> {
})));

out.push_str("</main>\n");
out.push_str(&footer(data));
out.push_str(&footer(data, &canonical));
out.push_str("</body>\n</html>\n");
Ok(out)
}
Expand Down Expand Up @@ -719,7 +731,7 @@ fn render_product(data: &SiteData, product: &Product) -> Result<String, String>
})));

out.push_str("</main>\n");
out.push_str(&footer(data));
out.push_str(&footer(data, &canonical));
out.push_str("</body>\n</html>\n");
Ok(out)
}
Expand Down
4 changes: 3 additions & 1 deletion about/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
table{border-collapse:collapse;width:100%;margin-top:8px;font-size:16px}
th,td{border-bottom:1px solid var(--line);text-align:left;padding:10px 8px;vertical-align:top}
</style>
<link rel="stylesheet" href="/assets/pages.css">
</head>
<body><main class="wrap">
<div class="top"><a class="brand" href="/">207 STUDIO</a><a href="https://207studio.dev/ko/about/" lang="ko" hreflang="ko">한국어</a></div>
Expand All @@ -58,5 +59,6 @@ <h2>Contact</h2>
<p>Email <a href="mailto:support@207studio.dev">support@207studio.dev</a> for support, licensing and Enterprise questions. We read every message.</p>
<p>Policies: <a href="/legal/terms/">Terms</a> · <a href="/legal/privacy/">Privacy</a> · <a href="/legal/refunds/">Refunds</a>. Code: <a href="https://github.com/207studio">github.com/207studio</a>.</p>
<p>For AI agents: <a href="/llms.txt">llms.txt</a> · <a href="/llms-full.txt">llms-full.txt</a>.</p>
</main></body>
</main><footer class="site-footer page-foot-hand"><div class="foot-grid"><div class="foot-brand"><a class="brand" href="/">207 Studio</a><p>Software for making and teaching.</p></div><nav class="foot-col" aria-label="Work"><h2>Work</h2><a href="/products/">Products</a><a href="/blog/">Writing</a><a href="https://apps.apple.com/app/id6815584225" target="_blank" rel="noopener">Pairy</a></nav><nav class="foot-col" aria-label="Studio"><h2>Studio</h2><a href="/about/">About</a><a href="mailto:support@207studio.dev">Support</a><a href="/llms.txt">llms.txt</a></nav><nav class="foot-col" aria-label="Legal"><h2>Legal</h2><a href="/legal/privacy/">Privacy Policy</a><a href="/legal/terms/">Terms of Use</a><a href="/legal/refunds/">Refund Policy</a></nav></div><img class="foot-word" src="/assets/footer-207.svg" alt="" aria-hidden="true" width="1513" height="700" loading="lazy" decoding="async"><div class="foot-bottom"><span>© 2026 207 Studio · South Korea</span></div></footer>
</body>
</html>
Loading
Loading