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
11 changes: 11 additions & 0 deletions gitlinq/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<html lang="ko">
<head>
<meta charset="utf-8" />
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-5WGTTWSF');</script>
<!-- End Google Tag Manager -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GitLinq 설명서가 이동했습니다</title>
<meta name="description" content="설치부터 변경 조회·취소와 서버 업데이트까지, 초보자 설명서를 회사 홈페이지에서 확인하세요." />
Expand All @@ -20,6 +27,10 @@
</style>
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5WGTTWSF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<p class="brand">DevsLab / GitLinq</p>
<h1>GitLinq 설명서가 이동했습니다</h1>
<p>설치부터 변경 조회·취소와 서버 업데이트까지, 초보자 설명서를 회사 홈페이지에서 확인하세요.</p>
Expand Down
11 changes: 11 additions & 0 deletions gitlinq/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<html lang="ko">
<head>
<meta charset="utf-8" />
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-5WGTTWSF');</script>
<!-- End Google Tag Manager -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GitLinq 소개 페이지가 이동했습니다</title>
<meta name="description" content="GitLinq 소개와 다운로드 안내는 DevsLab 회사 홈페이지에서 제공합니다." />
Expand All @@ -20,6 +27,10 @@
</style>
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5WGTTWSF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<p class="brand">DevsLab / GitLinq</p>
<h1>GitLinq 소개 페이지가 이동했습니다</h1>
<p>GitLinq 소개와 다운로드 안내는 DevsLab 회사 홈페이지에서 제공합니다.</p>
Expand Down
24 changes: 24 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
<html lang="ko">
<head>
<meta charset="utf-8" />
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-5WGTTWSF');</script>
<!-- End Google Tag Manager -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>데브스랩(DevsLab) 오픈소스 — 라이브 데모 허브</title>
<meta
Expand Down Expand Up @@ -324,6 +331,10 @@
</style>
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-5WGTTWSF"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<header class="top">
<div class="wrap top-inner">
<a href="https://devslab.kr" class="brand" aria-label="DevsLab">
Expand Down Expand Up @@ -541,6 +552,7 @@ <h3>devslab-examples</h3>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"WebSite","@id":"https://devslab-kr.github.io/#website","name":"데브스랩 오픈소스","alternateName":["DevsLab Open Source","devslab open source"],"url":"https://devslab-kr.github.io/","publisher":{"@id":"https://devslab.kr/#organization"}}</script>
<!-- publisher:end -->
<a href="https://github.com/devslab-kr">github.com/devslab.kr</a>
<a id="privacy-link" href="https://devslab.kr/privacy/">개인정보처리방침</a>
</div>
</div>
</footer>
Expand Down Expand Up @@ -596,6 +608,18 @@ <h3>devslab-examples</h3>
'pt-BR': 'Open source por DevsLab', fr: 'Open source par DevsLab', de: 'Open Source von DevsLab',
es: 'Código abierto por DevsLab', ar: 'مصدر مفتوح من DevsLab'
})[lang];
// The company policy (devslab.kr/privacy) covers this hub too. Its
// label follows devslab.kr's footer; ?lang= carries the reader's
// language across (devslab.kr shows Korean, or English otherwise).
var privacy = document.getElementById('privacy-link');
privacy.textContent = ({
ko: '개인정보처리방침', en: 'Privacy Policy', ja: 'プライバシーポリシー',
'zh-HK': '私隱政策', 'zh-TW': '隱私權政策', hi: 'गोपनीयता नीति',
vi: 'Chính sách quyền riêng tư', id: 'Kebijakan Privasi', th: 'นโยบายความเป็นส่วนตัว',
'pt-BR': 'Política de Privacidade', fr: 'Politique de confidentialité', de: 'Datenschutzerklärung',
es: 'Política de privacidad', ar: 'سياسة الخصوصية'
})[lang];
privacy.href = 'https://devslab.kr/privacy/?lang=' + encodeURIComponent(lang);
setAll('[data-i18n="live"]', t.live);
setAll('[data-i18n="docs"]', t.docs);
for (var key in t.cards) {
Expand Down
86 changes: 86 additions & 0 deletions tests/gtm.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import assert from 'node:assert/strict';
import { readdirSync, readFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';

const CONTAINER = 'GTM-5WGTTWSF';
const root = fileURLToPath(new URL('..', import.meta.url));
const SKIP = new Set(['.git', 'node_modules', '.worktrees', '.playwright-cli']);

// Every HTML file Pages serves, found by walking the tree rather than listed by
// hand, so a page added later fails here until it carries the container too.
function htmlFiles(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
if (SKIP.has(entry.name)) return [];
const path = join(dir, entry.name);
if (entry.isDirectory()) return htmlFiles(path);
return entry.name.endsWith('.html') ? [path] : [];
});
}

const pages = htmlFiles(root);

test('the walk finds the hub and its move pages', () => {
const found = pages.map((p) => relative(root, p).replaceAll('\\', '/')).sort();
for (const expected of ['index.html', 'gitlinq/index.html', 'gitlinq/docs/index.html']) {
assert.ok(found.includes(expected), `${expected} was not found`);
}
});

for (const page of pages) {
const name = relative(root, page).replaceAll('\\', '/');

test(`${name} loads GTM once, first thing after <meta charset>`, () => {
const html = readFileSync(page, 'utf8');
assert.equal(html.split('googletagmanager.com/gtm.js?id=').length - 1, 1, 'exactly one GTM loader');
const head = html.slice(html.indexOf('<head>'), html.indexOf('</head>'));
const afterCharset = head.slice(head.indexOf('<meta charset="utf-8" />') + '<meta charset="utf-8" />'.length);
assert.ok(head.includes('<meta charset="utf-8" />'), 'the page declares its charset');
// Only whitespace and Google's own marker comment may sit between them.
assert.match(afterCharset, /^\s*<!-- Google Tag Manager -->\s*<script>\(function\(w,d,s,l,i\)/);
const loader = afterCharset.match(/<script>([\s\S]*?)<\/script>/)[1];
assert.ok(loader.includes(`'dataLayer','${CONTAINER}'`), `the loader names ${CONTAINER}`);

// Run it: it must queue gtm.start and request gtm.js for this container.
const inserted = [];
const document = {
getElementsByTagName: () => [{ parentNode: { insertBefore: (el) => inserted.push(el) } }],
createElement: () => ({}),
};
const window = {};
runInNewContext(loader, { window, document, Date });
assert.equal(window.dataLayer[0].event, 'gtm.js');
assert.equal(inserted.length, 1);
assert.equal(inserted[0].async, true);
assert.equal(inserted[0].src, `https://www.googletagmanager.com/gtm.js?id=${CONTAINER}`);
});

test(`${name} opens <body> with the GTM noscript`, () => {
const html = readFileSync(page, 'utf8');
assert.equal(html.split('googletagmanager.com/ns.html?id=').length - 1, 1, 'exactly one GTM noscript');
const afterBody = html.slice(html.indexOf('<body>') + '<body>'.length);
const firstElement = afterBody.replace(/^(\s|<!--[\s\S]*?-->)*/, '');
assert.match(
firstElement,
new RegExp(`^<noscript><iframe src="https://www\\.googletagmanager\\.com/ns\\.html\\?id=${CONTAINER}"`),
);
});
}

test('the hub footer links the company privacy policy in every hub locale', () => {
const html = readFileSync(join(root, 'index.html'), 'utf8');
const link = html.match(/<a id="privacy-link" href="([^"]+)">([^<]+)<\/a>/);
assert.ok(link, 'the footer carries the privacy link');
assert.equal(link[1], 'https://devslab.kr/privacy/');
assert.equal(link[2], '개인정보처리방침');
assert.ok(html.slice(html.indexOf('<footer>'), html.indexOf('</footer>')).includes(link[0]), 'it sits in the footer');

const supported = JSON.parse(html.match(/var SUPPORTED = (\[[^\]]+\]);/)[1].replaceAll("'", '"'));
const labels = html.match(/privacy\.textContent = \((\{[\s\S]*?\})\)\[lang\];/)[1];
const map = runInNewContext(`(${labels})`);
assert.deepEqual(Object.keys(map).sort(), [...supported].sort(), 'one label per hub locale');
for (const lang of supported) assert.ok(map[lang].trim(), `${lang} has a label`);
assert.ok(!/↗/.test(link[0]) && !Object.values(map).some((l) => l.includes('↗')), 'no external-link arrow');
});
Loading