diff --git a/create-posts.mjs b/create-posts.mjs new file mode 100644 index 0000000..c11b259 --- /dev/null +++ b/create-posts.mjs @@ -0,0 +1,223 @@ +/** + * One-shot script to seed the Battl Builders author + first two posts. + * + * Uses Node.js's native https module and connects directly to the Sanity API + * IP address to bypass DNS resolution issues (ENOTFOUND on .api.sanity.io). + * TLS SNI + Host header ensure routing and certificate validation still work. + * + * Run: node create-posts.mjs + */ +import https from 'https' + +const TOKEN = 'ski8JOhwjVobqqNTo2WTnLTtZAwP96LC1XI5cdA1t68vf5um2aQyvxtWfjHwFZKzj8QKVeOBOP0lBhCdzFzqPxL4PG6fpTfUA9QGpAUBBM2RgXnwNMdvFGASfv0U5UWcu0KJx8E8rHoCfrMlhmAnuGRoUqnqhASf1hogiefNt8CkCLEcggGz' +const PROJECT = 'zal102rv' +const DATASET = 'production' +const API_VER = '2024-01-01' +const API_HOST = `${PROJECT}.api.sanity.io` +const API_IP = '34.102.242.91' // resolved IP — bypasses DNS + +// ── Transport ───────────────────────────────────────────────────────────────── + +function post(path, body) { + return new Promise((resolve, reject) => { + const payload = JSON.stringify(body) + const req = https.request( + { + method: 'POST', + hostname: API_IP, // connect by IP — no DNS needed + port: 443, + path, + servername: API_HOST, // TLS SNI — tells server which cert to present + headers: { + Host: API_HOST, // HTTP/1.1 routing header + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload), + Authorization: `Bearer ${TOKEN}`, + Accept: 'application/json', + 'User-Agent': 'battl-seed/1.0', + }, + }, + (res) => { + let data = '' + res.on('data', (chunk) => (data += chunk)) + res.on('end', () => { + try { + const parsed = JSON.parse(data) + if (res.statusCode >= 400) { + reject(new Error(`HTTP ${res.statusCode}: ${JSON.stringify(parsed)}`)) + } else { + resolve(parsed) + } + } catch { + reject(new Error(`Non-JSON response (${res.statusCode}): ${data.slice(0, 200)}`)) + } + }) + } + ) + req.on('error', reject) + req.write(payload) + req.end() + }) +} + +function mutate(mutations) { + return post( + `/v${API_VER}/data/mutate/${DATASET}?returnIds=true&returnDocuments=true&visibility=sync`, + { mutations } + ) +} + +function createOrReplace(doc) { + return mutate([{ createOrReplace: doc }]) +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function block(text, style = 'normal') { + return { + _type: 'block', + _key: Math.random().toString(36).slice(2), + style, + markDefs: [], + children: [{ _type: 'span', _key: Math.random().toString(36).slice(2), text, marks: [] }], + } +} + +function h2(text) { return block(text, 'h2') } +function h3(text) { return block(text, 'h3') } +function p(text) { return block(text, 'normal') } + +// ── Author ──────────────────────────────────────────────────────────────────── + +const authorId = 'author-battl-team' + +// ── Post 1 ─────────────────────────────────────────────────────────────────── + +const post1 = { + _type: 'post', + _id: 'post-ar15-every-part-explained', + title: 'How to Build an AR-15: Every Part Explained', + slug: { _type: 'slug', current: 'how-to-build-an-ar15-every-part-explained' }, + author: { _type: 'reference', _ref: authorId }, + publishedAt: new Date().toISOString(), + excerpt: + 'Lower, upper, barrel, handguard, BCG — there are a lot of parts in an AR-15 build. This guide breaks down every slot in the Battl Builder and explains exactly what each part does and why it matters.', + seoTitle: 'How to Build an AR-15: Every Part Explained | Battl Builders', + seoDescription: + 'A plain-language guide to every part in an AR-15 build — lower, upper, barrel, BCG, handguard, stock, trigger, and more. Learn what each part does before you buy.', + body: [ + p('An AR-15 build has more named parts than most people expect — and that list gets longer when you realise many "parts" are actually assemblies of smaller components. But the build process is more modular than it looks. Once you understand what each slot does, the whole thing clicks into place.'), + p('This guide walks through every slot in the Battl Builder, in the order you\'d typically spec a rifle, and explains what each part does and why you care about it.'), + + h2('Lower Receiver'), + p('The lower receiver is the serialised part — the legal firearm. Everything else attaches to it. Lowers are almost universally mil-spec pattern for AR-15 builds, which is why compatibility matters less here than in other slots. Forged 7075-T6 aluminium is the most common material. You\'ll often buy a stripped lower and assemble it yourself, or buy a complete lower with the LPK and stock already installed.'), + + h2('Lower Parts Kit (LPK)'), + p('The LPK is the bag of small parts that lives inside the lower: trigger, hammer, disconnector, safety selector, mag release, bolt catch, takedown pins, and buffer retainer. Stock mil-spec LPKs are reliable and cheap. If you want a better trigger pull, you upgrade the trigger group — but that\'s a separate slot in the builder.'), + + h2('Trigger'), + p('The trigger group is the one LPK component worth upgrading separately. Mil-spec triggers average a 6–8 lb pull with some grittiness. Aftermarket single-stage triggers (Geissele, Rise Armament, CMC) bring that down to 3–4 lb with a cleaner break. Two-stage triggers add a light take-up stage before the break — preferred by many precision shooters. This is one of the highest-ROI upgrades on a budget build.'), + + h2('Buffer Tube, Buffer & Spring'), + p('The buffer system lives in the stock and absorbs the rearward energy of each shot, then returns the bolt carrier forward. Buffer weight (carbine, H, H2, H3) is matched to your gas system length and calibre — heavier buffers slow the bolt velocity, which matters if you\'re running a suppressor or an adjustable gas block. Carbine buffers pair with carbine stocks; rifle-length buffers pair with fixed A2-style stocks.'), + + h2('Stock'), + p('The stock is what goes against your shoulder. Mil-spec tube stocks (6-position collapsible) are the default. Fixed stocks (A2, Magpul PRS) give a more stable cheek weld for precision builds. If you\'re building a pistol configuration, you\'ll use a pistol brace instead — and your buffer tube becomes a pistol buffer tube, which changes diameter.'), + + h2('Upper Receiver'), + p('The upper receiver is the top half of the rifle. It houses the barrel extension, accepts the handguard, and is where the bolt carrier group sits during cycling. Flattop uppers with a Picatinny rail are standard. Like lowers, forged 7075-T6 is the common material. Stripped uppers require you to install the barrel and BCG; complete uppers come assembled. Buying a matched upper/lower set from the same manufacturer avoids any fit issues.'), + + h2('Barrel'), + p('The barrel is where calibre, gas system length, twist rate, and profile all intersect — and it\'s arguably the most build-defining part you\'ll choose.'), + p('Gas system length (pistol, carbine, mid-length, rifle) should match your barrel length. Mid-length gas on a 16" barrel is a popular, softer-shooting combination. Twist rate (1:7, 1:8, 1:9) determines what bullet weights stabilise well — 1:8 is the most versatile. Profile (light, government, SOCOM, heavy) affects weight and heat dissipation.'), + + h2('Handguard / Rail'), + p('The handguard protects your support hand from a hot barrel and gives you somewhere to mount accessories. M-LOK and KeyMod are the two attachment systems; M-LOK has largely won. Free-floating handguards (attached only at the upper receiver) improve accuracy by removing barrel pressure. Drop-in handguards are easier to install and cheaper. Length should match or complement your gas system without covering the gas block.'), + + h2('Gas Block & Gas Tube'), + p('The gas block sits on the barrel at the gas port and directs propellant gas back into the gas tube to cycle the action. Low-profile gas blocks sit under the handguard and accept sling mounts and adjustable gas valves. Adjustable gas blocks (Superlative Arms, SLR Rifleworks) let you tune gas flow — essential for suppressed use or very light/heavy loads.'), + + h2('Bolt Carrier Group (BCG)'), + p('The BCG is the heart of the cycling mechanism: bolt, carrier, cam pin, firing pin. It strips a round from the magazine, chambers it, fires it, extracts the spent case, and resets. Quality matters here — look for properly staked gas keys, properly cut/gauged bolt heads, and good carrier finish (nitride/phosphate/nickel boron). This is not the place to save $20.'), + + h2('Charging Handle'), + p('The charging handle is how you manually operate the bolt — chamber a round, clear a malfunction, lock the bolt back. Mil-spec handles work fine. Extended latch handles (BCM Gunfighter, Radian Raptor) make one-handed manipulation easier, especially with optics or suppressors that push your head position back. A minor upgrade with a real quality-of-life difference.'), + + h2('Muzzle Device'), + p('The muzzle device threads onto the barrel\'s muzzle end. Flash hiders reduce the signature of each shot. Compensators reduce muzzle rise. Muzzle brakes reduce felt recoil more aggressively but are loud. Suppressors attach over the muzzle (either direct-thread or via a muzzle device mount). Thread pitch must match your barrel — 1/2×28 is standard for 5.56/.223; 5/8×24 is standard for .308/6.5 Creedmoor.'), + + p('That\'s every major slot in a standard AR-15 build. If you\'re ready to start speccing parts, the Battl Builder walks you through each slot with real retailer pricing.'), + ], +} + +// ── Post 2 ─────────────────────────────────────────────────────────────────── + +const post2 = { + _type: 'post', + _id: 'post-complete-upper-vs-build', + title: 'Complete Upper vs. Build From Parts: Which Path Is Right for You?', + slug: { _type: 'slug', current: 'complete-upper-vs-build-from-parts' }, + author: { _type: 'reference', _ref: authorId }, + publishedAt: new Date(Date.now() - 86400000).toISOString(), + excerpt: + 'Should you buy a complete upper or source every component separately? Both paths get you to the same rifle — but the right choice depends on your budget, timeline, and how much you actually want to wrench.', + seoTitle: 'Complete Upper vs. Build From Parts: Which Path Is Right for You? | Battl Builders', + seoDescription: + 'Buying a complete upper is faster and often cheaper. Building from individual parts gives you more control. Here\'s how to decide which approach fits your AR-15 build.', + body: [ + p('Every AR-15 build starts with the same question: do you buy a complete upper assembly, or do you source the barrel, handguard, BCG, and charging handle separately?'), + p('Both approaches work. The answer depends on three things: your budget, how much customisation you actually want, and whether you have (or want to acquire) the tools and confidence to headspace a barrel and torque a barrel nut correctly.'), + + h2('The Case for a Complete Upper'), + p('A complete upper — barrel, handguard, BCG, charging handle, and often muzzle device already assembled — is the faster, lower-friction path. It usually costs less than the sum of individual parts because manufacturers buy components in volume. And it eliminates the headspacing step, since the barrel is already installed and (from a reputable builder) already gauged.'), + h3('When a complete upper makes sense:'), + p('You\'re building your first AR and don\'t yet own a barrel nut wrench, torque wrench, or headspace gauges. You want to get to the range quickly. You\'re working with a modest budget and the complete upper from your chosen manufacturer is priced right. The manufacturer offers a specific configuration — barrel length, gas system, profile, calibre — that matches what you want.'), + p('Brands like BCM, Daniel Defense, Aero Precision, and PSA all sell complete uppers at different price points with predictable quality. A BCM or Daniel Defense upper is ready to drop onto a lower and shoot — no additional gunsmithing required.'), + + h2('The Case for Building From Parts'), + p('Sourcing each component separately gives you full control: you pick the exact barrel, the exact handguard, and the exact BCG without being constrained by what any one manufacturer packages together. It also lets you take advantage of sales and component-level deals that don\'t apply to complete assemblies.'), + h3('When parting it out makes sense:'), + p('You have a specific configuration in mind that no manufacturer packages — say, a Ballistic Advantage barrel on a Aero Precision upper with an SLR adjustable gas block and a Midwest Industries handguard. You already own or are willing to buy the armorer\'s tools (barrel nut wrench set, torque wrench, headspace go/no-go gauges, upper vice block). You\'re building multiple rifles and the per-build tool cost amortises out. You want to understand the platform deeply — there\'s no better way to learn than assembling from scratch.'), + p('The tool investment is the real barrier. A decent barrel nut wrench set, torque wrench, upper receiver vice block, and headspace gauges will run $150–250. On a single build that\'s a meaningful addition to cost. On a second or third build, those tools have already paid for themselves.'), + + h2('What the Battl Builder Can Do For Both Paths'), + p('The Battl Builder lets you spec an upper either way. If you want a complete upper, select it as a single unit and your remaining slots (lower, stock, trigger, etc.) fill in around it. If you want to build from individual components, use the individual slots for barrel, handguard, BCG, and charging handle.'), + p('Either way, the builder shows you live pricing from current retailers and keeps a running total so you can see exactly where your budget is going — and whether that premium BCG is worth it compared to a mil-spec option.'), + + h2('A Word on Headspacing'), + p('This comes up every time: do I need to headspace if I buy a complete upper? No. Headspacing is done during barrel installation. If you\'re buying a complete upper from a reputable manufacturer, that\'s already been checked. If you\'re installing your own barrel, you must headspace it with go and no-go gauges before shooting. This is non-negotiable — an improperly headspaced rifle is a safety issue.'), + + p('Whether you go complete upper or build from parts, the Battl Builder is designed to help you think through the decision before you spend a dollar. Start with what you know — calibre, intended use, budget — and let the builder help you fill in the rest.'), + ], +} + +// ── Run ─────────────────────────────────────────────────────────────────────── + +async function main() { + console.log(`Connecting directly to ${API_IP} (bypassing DNS for ${API_HOST})…\n`) + + console.log('Creating author…') + await createOrReplace({ + _type: 'author', + _id: authorId, + name: 'Battl Builders Team', + bio: 'The Battl Builders team — operators, builders, and gear nerds working to make AR-15 builds less confusing.', + }) + console.log('✓ Author created') + + console.log('Creating post 1…') + await createOrReplace(post1) + console.log('✓ Post 1 created (draft)') + + console.log('Creating post 2…') + await createOrReplace(post2) + console.log('✓ Post 2 created (draft)') + + console.log('\nAll done. Open Sanity Studio and publish both drafts.') + console.log('Then delete this file — it contains your write token.') +} + +main().catch((err) => { + console.error('Failed:', err.message) + process.exit(1) +}) diff --git a/schemaTypes/author.ts b/schemaTypes/author.ts new file mode 100644 index 0000000..e715aef --- /dev/null +++ b/schemaTypes/author.ts @@ -0,0 +1,36 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'author', + title: 'Author', + type: 'document', + fields: [ + defineField({ + name: 'name', + title: 'Name', + type: 'string', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'slug', + title: 'Slug', + type: 'slug', + options: {source: 'name'}, + }), + defineField({ + name: 'image', + title: 'Image', + type: 'image', + options: {hotspot: true}, + }), + defineField({ + name: 'bio', + title: 'Bio', + type: 'text', + rows: 3, + }), + ], + preview: { + select: {title: 'name', media: 'image'}, + }, +}) diff --git a/schemaTypes/index.ts b/schemaTypes/index.ts index ba4681d..aa24b42 100644 --- a/schemaTypes/index.ts +++ b/schemaTypes/index.ts @@ -1 +1,4 @@ -export const schemaTypes = [] +import author from './author' +import post from './post' + +export const schemaTypes = [author, post] diff --git a/schemaTypes/post.ts b/schemaTypes/post.ts new file mode 100644 index 0000000..783d0ab --- /dev/null +++ b/schemaTypes/post.ts @@ -0,0 +1,118 @@ +import {defineField, defineType} from 'sanity' + +export default defineType({ + name: 'post', + title: 'Post', + type: 'document', + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + validation: (Rule) => Rule.required().max(100), + }), + defineField({ + name: 'slug', + title: 'Slug', + type: 'slug', + options: {source: 'title', maxLength: 96}, + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'author', + title: 'Author', + type: 'reference', + to: [{type: 'author'}], + }), + defineField({ + name: 'mainImage', + title: 'Main Image', + type: 'image', + options: {hotspot: true}, + fields: [ + defineField({ + name: 'alt', + title: 'Alt Text', + type: 'string', + description: 'Describe the image for screen readers and SEO.', + }), + ], + }), + defineField({ + name: 'publishedAt', + title: 'Published At', + type: 'datetime', + initialValue: () => new Date().toISOString(), + }), + defineField({ + name: 'excerpt', + title: 'Excerpt', + type: 'text', + rows: 3, + description: 'Short summary shown in listings and used as meta description (~160 chars).', + validation: (Rule) => Rule.max(200), + }), + defineField({ + name: 'body', + title: 'Body', + type: 'array', + of: [ + {type: 'block'}, + { + type: 'image', + options: {hotspot: true}, + fields: [ + defineField({ + name: 'alt', + title: 'Alt Text', + type: 'string', + }), + defineField({ + name: 'caption', + title: 'Caption', + type: 'string', + }), + ], + }, + ], + }), + defineField({ + name: 'seoTitle', + title: 'SEO Title', + type: 'string', + description: 'Overrides page title in
. Leave blank to use post title.', + validation: (Rule) => Rule.max(70), + }), + defineField({ + name: 'seoDescription', + title: 'SEO Description', + type: 'text', + rows: 2, + description: 'Overrides excerpt in . Leave blank to use excerpt.', + validation: (Rule) => Rule.max(160), + }), + ], + orderings: [ + { + title: 'Published Date, Newest', + name: 'publishedAtDesc', + by: [{field: 'publishedAt', direction: 'desc'}], + }, + ], + preview: { + select: { + title: 'title', + author: 'author.name', + media: 'mainImage', + publishedAt: 'publishedAt', + }, + prepare({title, author, media, publishedAt}) { + const date = publishedAt ? new Date(publishedAt).toLocaleDateString() : 'Draft' + return { + title, + subtitle: `${author ?? 'No author'} · ${date}`, + media, + } + }, + }, +})