Skip to content

Commit 3f1bb09

Browse files
committed
feat(seo): add JSON-LD structured data injection and FAQ section prototype
- Create schema-inject.js for dynamic JSON-LD (TechArticle, FAQPage, BreadcrumbList, WebSite) - Add SEO frontmatter to scope-and-closures page (og:type, article:author, article:tag) - Add FAQ section with 6 search-optimized questions for GEO visibility - Script runs on all pages, detects page type from URL, extracts data from DOM
1 parent 22dfd46 commit 3f1bb09

2 files changed

Lines changed: 295 additions & 1 deletion

File tree

docs/concepts/scope-and-closures.mdx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
---
22
title: "Scope & Closures"
33
sidebarTitle: "Scope and Closures: How Variables Really Work"
4-
description: "Learn JavaScript scope and closures. Understand var vs let vs const, lexical scoping, the scope chain, and closure patterns."
4+
description: "Learn JavaScript scope and closures. Understand the three types of scope, var vs let vs const, lexical scoping, the scope chain, and closure patterns for data privacy."
5+
"og:type": "article"
6+
"article:author": "Leonardo Maldonado"
7+
"article:section": "JavaScript Fundamentals"
8+
"article:tag": "javascript closures, javascript scope, var let const, lexical scope, scope chain, closure patterns"
59
---
610

711
Why can some variables be accessed from anywhere in your code, while others seem to disappear? How do functions "remember" variables from their parent functions, even after those functions have finished running?
@@ -1082,6 +1086,44 @@ cleanup(); // Removes listener, allows memory to be freed
10821086

10831087
---
10841088

1089+
## Frequently Asked Questions
1090+
1091+
<AccordionGroup>
1092+
<Accordion title="What is the difference between scope and closures in JavaScript?">
1093+
Scope defines where a variable can be accessed in your code. A closure is what happens when a function keeps access to variables from its outer lexical scope even after that outer function returns. In short: scope is the rulebook, closure is a practical behavior created by those rules.
1094+
</Accordion>
1095+
1096+
<Accordion title="Why should I use let and const instead of var?">
1097+
`let` and `const` are block-scoped, so they reduce accidental leaks and make intent clearer. `const` communicates that the binding should not be reassigned, while `let` is for values that change. `var` is function-scoped and hoisted in ways that often produce bugs, especially inside loops and conditionals.
1098+
</Accordion>
1099+
1100+
<Accordion title="How do closures work in JavaScript?">
1101+
A function closes over the variables available where it was defined, not where it is called. When that function runs later, JavaScript still resolves those captured variables through the saved lexical environment.
1102+
1103+
```javascript
1104+
function makeGreeter(name) {
1105+
return function greet() {
1106+
return `Hi, ${name}`;
1107+
};
1108+
}
1109+
```
1110+
</Accordion>
1111+
1112+
<Accordion title="What are common use cases for closures?">
1113+
Common uses include data privacy, function factories, memoization, and stateful callbacks. As Kyle Simpson explains in *You Don't Know JS: Scope & Closures*, closures are not a niche feature; they are a core part of how JavaScript functions work. You will use closures any time a callback needs to remember context.
1114+
</Accordion>
1115+
1116+
<Accordion title="What is lexical scope vs dynamic scope?">
1117+
JavaScript uses lexical scope, which means variable access is decided by where code is written in the file. Dynamic scope would decide variable access based on the call stack at runtime, but JavaScript does not use that model. This is why moving a function changes what it can access, even if calls stay the same.
1118+
</Accordion>
1119+
1120+
<Accordion title="Can closures cause memory leaks?">
1121+
Closures can keep objects in memory longer than expected if they retain references you no longer need. This is most common with long-lived event listeners and timers that capture large data structures. In the 2023 State of JS survey, many developers still reported debugging memory/performance issues, so cleaning up listeners and limiting captured data is an important habit.
1122+
</Accordion>
1123+
</AccordionGroup>
1124+
1125+
---
1126+
10851127
## Related Concepts
10861128

10871129
<CardGroup cols={2}>

docs/schema-inject.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
(function () {
2+
var SITE_NAME = "33 JavaScript Concepts";
3+
var SITE_URL = "https://33jsconcepts.com";
4+
var AUTHOR = {
5+
"@type": "Person",
6+
name: "Leonardo Maldonado",
7+
url: "https://github.com/leonardomso",
8+
};
9+
var PUBLISHER = {
10+
"@type": "Organization",
11+
name: SITE_NAME,
12+
url: SITE_URL,
13+
};
14+
15+
function safeText(value) {
16+
return (value || "").replace(/\s+/g, " ").trim();
17+
}
18+
19+
function toTitle(segment) {
20+
var cleaned = decodeURIComponent(segment || "").replace(/[-_]+/g, " ").trim();
21+
if (!cleaned) return "";
22+
return cleaned
23+
.split(" ")
24+
.map(function (word) {
25+
return word.charAt(0).toUpperCase() + word.slice(1);
26+
})
27+
.join(" ");
28+
}
29+
30+
function getCanonicalUrl() {
31+
var canonical = document.querySelector('link[rel="canonical"]');
32+
if (canonical && canonical.href) return canonical.href;
33+
return new URL(window.location.pathname + window.location.search, SITE_URL).toString();
34+
}
35+
36+
function getDescription() {
37+
var meta = document.querySelector('meta[name="description"]');
38+
return safeText(meta && meta.content);
39+
}
40+
41+
function getDatePublished() {
42+
var publishedMeta = document.querySelector('meta[property="article:published_time"]');
43+
if (publishedMeta && publishedMeta.content) return publishedMeta.content;
44+
45+
var timeEl = document.querySelector("time[datetime]");
46+
if (timeEl) {
47+
var dateTime = timeEl.getAttribute("datetime");
48+
if (dateTime) return dateTime;
49+
}
50+
51+
if (document.lastModified) {
52+
var parsed = new Date(document.lastModified);
53+
if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
54+
}
55+
56+
return new Date().toISOString();
57+
}
58+
59+
function isConceptArticle(pathname) {
60+
return /^\/(concepts|beyond\/concepts)\/.+/.test(pathname);
61+
}
62+
63+
function buildBreadcrumbList(pathname) {
64+
var parts = pathname.split("/").filter(Boolean);
65+
var itemListElement = [
66+
{
67+
"@type": "ListItem",
68+
position: 1,
69+
name: "Home",
70+
item: SITE_URL + "/",
71+
},
72+
];
73+
74+
var runningPath = "";
75+
for (var i = 0; i < parts.length; i += 1) {
76+
runningPath += "/" + parts[i];
77+
itemListElement.push({
78+
"@type": "ListItem",
79+
position: i + 2,
80+
name: toTitle(parts[i]),
81+
item: SITE_URL + runningPath,
82+
});
83+
}
84+
85+
return {
86+
"@type": "BreadcrumbList",
87+
itemListElement: itemListElement,
88+
};
89+
}
90+
91+
function findFaqHeading() {
92+
var headings = Array.prototype.slice.call(document.querySelectorAll("h2"));
93+
return (
94+
headings.find(function (heading) {
95+
return safeText(heading.textContent).toLowerCase().indexOf("frequently asked questions") !== -1;
96+
}) || null
97+
);
98+
}
99+
100+
function getFaqSectionNodes(heading) {
101+
var nodes = [];
102+
var cursor = heading ? heading.nextElementSibling : null;
103+
while (cursor) {
104+
if (cursor.tagName === "H2") break;
105+
nodes.push(cursor);
106+
cursor = cursor.nextElementSibling;
107+
}
108+
return nodes;
109+
}
110+
111+
function extractFaqItems() {
112+
var heading = findFaqHeading();
113+
if (!heading) return [];
114+
115+
var sectionNodes = getFaqSectionNodes(heading);
116+
if (!sectionNodes.length) return [];
117+
118+
var questions = [];
119+
var seen = new Set();
120+
var triggerSelector = [
121+
"button",
122+
"summary",
123+
"[role='button']",
124+
"[aria-controls]",
125+
"[data-state]",
126+
].join(",");
127+
128+
sectionNodes.forEach(function (node) {
129+
var triggers = Array.prototype.slice.call(node.querySelectorAll(triggerSelector));
130+
131+
triggers.forEach(function (trigger) {
132+
var questionText = safeText(trigger.textContent);
133+
if (!questionText || questionText.length < 10) return;
134+
if (seen.has(questionText)) return;
135+
136+
var answerText = "";
137+
var controlsId = trigger.getAttribute("aria-controls");
138+
if (controlsId) {
139+
var controlled = document.getElementById(controlsId);
140+
answerText = safeText(controlled && controlled.textContent);
141+
}
142+
143+
if (!answerText) {
144+
var itemRoot = trigger.closest("[data-radix-collection-item], details, li, div");
145+
if (itemRoot) {
146+
var answerCandidate = Array.prototype.slice
147+
.call(itemRoot.querySelectorAll("p, div"))
148+
.map(function (el) {
149+
if (el === trigger || el.contains(trigger)) return "";
150+
return safeText(el.textContent);
151+
})
152+
.find(function (text) {
153+
return text && text.length > 20;
154+
});
155+
answerText = answerCandidate || "";
156+
}
157+
}
158+
159+
if (!answerText) return;
160+
161+
seen.add(questionText);
162+
questions.push({
163+
"@type": "Question",
164+
name: questionText,
165+
acceptedAnswer: {
166+
"@type": "Answer",
167+
text: answerText,
168+
},
169+
});
170+
});
171+
});
172+
173+
return questions;
174+
}
175+
176+
function buildGraph() {
177+
var pathname = window.location.pathname || "/";
178+
var headline = safeText(document.title);
179+
var description = getDescription();
180+
var canonicalUrl = getCanonicalUrl();
181+
var graph = [];
182+
183+
if (pathname === "/") {
184+
graph.push({
185+
"@type": "WebSite",
186+
name: SITE_NAME,
187+
description: description,
188+
url: SITE_URL,
189+
potentialAction: {
190+
"@type": "SearchAction",
191+
target: SITE_URL + "/search?q={search_term_string}",
192+
"query-input": "required name=search_term_string",
193+
},
194+
});
195+
} else if (isConceptArticle(pathname)) {
196+
graph.push({
197+
"@type": "TechArticle",
198+
headline: headline,
199+
description: description,
200+
author: AUTHOR,
201+
publisher: PUBLISHER,
202+
datePublished: getDatePublished(),
203+
url: canonicalUrl,
204+
mainEntityOfPage: canonicalUrl,
205+
});
206+
} else {
207+
graph.push({
208+
"@type": "WebPage",
209+
name: headline,
210+
description: description,
211+
url: canonicalUrl,
212+
});
213+
}
214+
215+
graph.push(buildBreadcrumbList(pathname));
216+
217+
var faqItems = extractFaqItems();
218+
if (faqItems.length) {
219+
graph.push({
220+
"@type": "FAQPage",
221+
mainEntity: faqItems,
222+
});
223+
}
224+
225+
return graph;
226+
}
227+
228+
function injectSchema() {
229+
try {
230+
var graph = buildGraph();
231+
if (!graph.length) return;
232+
233+
var existing = document.getElementById("structured-data-jsonld");
234+
if (existing) existing.remove();
235+
236+
var script = document.createElement("script");
237+
script.id = "structured-data-jsonld";
238+
script.type = "application/ld+json";
239+
script.text = JSON.stringify({
240+
"@context": "https://schema.org",
241+
"@graph": graph,
242+
});
243+
document.head.appendChild(script);
244+
} catch (_error) {}
245+
}
246+
247+
if (document.readyState === "loading") {
248+
document.addEventListener("DOMContentLoaded", injectSchema);
249+
} else {
250+
injectSchema();
251+
}
252+
})();

0 commit comments

Comments
 (0)